feat: Enhance mission management with CyBOK integration and collection filtering

- Added `Cybok` model to manage CyBOK entries associated with missions.
- Implemented `by_collection` scope in `Mission` model for filtering missions by collection.
- Updated `missions_controller` to filter missions based on the selected collection.
- Introduced `CybokSyncService` for syncing CyBOK data from mission metadata to the database.
- Created new views and partials for displaying CyBOK information with tooltips using Tippy.js.
- Added metadata fields to `break_escape_missions` for `secgen_scenario` and `collection`.
- Enhanced mission seeding logic to support new metadata and CyBOK entries.
- Added multiple new mission scenarios with associated metadata.
This commit is contained in:
Z. Cliffe Schreuders
2025-11-25 15:20:05 +00:00
parent 797b075cbe
commit 3cc9fafcec
41 changed files with 906 additions and 15 deletions

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 30" width="100" height="30">
<rect x="0" y="0" width="100" height="30" fill="none"/>
<text x="5" y="22" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="white">CyBOK</text>
</svg>

After

Width:  |  Height:  |  Size: 264 B

View File

@@ -0,0 +1,63 @@
/*
* BreakEscape Label Styles
* Mirrors Hacktivity's label styling for consistency
*/
/* Base label styles */
.label {
display: inline-block;
padding: 4px 8px;
font-size: 12px;
font-weight: 600;
border-radius: 4px;
cursor: default;
}
/* CyBOK label specific styling */
.label-cybok {
background-color: #2d5a27;
color: #ffffff;
border: 1px solid #3d7a37;
}
.label-cybok:hover {
background-color: #3d7a37;
}
/* Difficulty labels */
.label-difficulty {
background-color: #00ff00;
color: #000000;
}
.label-difficulty-1 {
background-color: #4ade80;
color: #000000;
}
.label-difficulty-2 {
background-color: #22c55e;
color: #000000;
}
.label-difficulty-3 {
background-color: #f59e0b;
color: #000000;
}
.label-difficulty-4 {
background-color: #ef4444;
color: #ffffff;
}
.label-difficulty-5 {
background-color: #7c2d12;
color: #ffffff;
}
/* Collection labels */
.label-collection {
background-color: #3b82f6;
color: #ffffff;
border: 1px solid #2563eb;
}

View File

@@ -0,0 +1,76 @@
/*
* BreakEscape Tooltip Styles
* Mirrors Hacktivity's Tippy.js tooltip theming
*/
/* Hacktivity-style theme for Tippy tooltips */
.tippy-box[data-theme~='break-escape'] {
border: 2px solid grey;
box-shadow: inset 0px 0px 0px 1px grey;
background-color: #2a2a2a;
color: #ffffff;
}
.tippy-box[data-theme~='break-escape'][data-placement^='top'] > .tippy-arrow::before {
border-top-color: grey;
}
.tippy-box[data-theme~='break-escape'][data-placement^='bottom'] > .tippy-arrow::before {
border-bottom-color: grey;
}
.tippy-box[data-theme~='break-escape'][data-placement^='left'] > .tippy-arrow::before {
border-left-color: grey;
}
.tippy-box[data-theme~='break-escape'][data-placement^='right'] > .tippy-arrow::before {
border-right-color: grey;
}
/* Dark theme variant */
.tippy-box[data-theme~='break-escape-dark'] {
border: 2px solid #333333;
background-color: #333333;
box-shadow: inset 0px 0px 0px 1px #333333;
color: white;
}
.tippy-box[data-theme~='break-escape-dark'][data-placement^='top'] > .tippy-arrow::before {
border-top-color: #333333;
}
.tippy-box[data-theme~='break-escape-dark'][data-placement^='bottom'] > .tippy-arrow::before {
border-bottom-color: #333333;
}
.tippy-box[data-theme~='break-escape-dark'][data-placement^='left'] > .tippy-arrow::before {
border-left-color: #333333;
}
.tippy-box[data-theme~='break-escape-dark'][data-placement^='right'] > .tippy-arrow::before {
border-right-color: #333333;
}
/* Green accent theme matching BreakEscape branding */
.tippy-box[data-theme~='break-escape-green'] {
border: 2px solid #00ff00;
background-color: #1a1a1a;
box-shadow: inset 0px 0px 0px 1px #00ff00;
color: #ffffff;
}
.tippy-box[data-theme~='break-escape-green'][data-placement^='top'] > .tippy-arrow::before {
border-top-color: #00ff00;
}
.tippy-box[data-theme~='break-escape-green'][data-placement^='bottom'] > .tippy-arrow::before {
border-bottom-color: #00ff00;
}
.tippy-box[data-theme~='break-escape-green'][data-placement^='left'] > .tippy-arrow::before {
border-left-color: #00ff00;
}
.tippy-box[data-theme~='break-escape-green'][data-placement^='right'] > .tippy-arrow::before {
border-right-color: #00ff00;
}

View File

@@ -6,6 +6,14 @@ module BreakEscape
else
Mission.published
end
# Filter by collection if specified
if params[:collection].present?
@missions = @missions.by_collection(params[:collection])
end
# Eager load CyBOK data for display
@missions = @missions.includes(:break_escape_cyboks)
end
def show

View File

@@ -1,4 +1,8 @@
module BreakEscape
module ApplicationHelper
# Generate a random ID for DOM elements
def generate_random_id
SecureRandom.hex(8)
end
end
end

View File

@@ -0,0 +1,63 @@
# frozen_string_literal: true
module BreakEscape
class Cybok < ApplicationRecord
self.table_name = 'break_escape_cyboks'
belongs_to :cybokable, polymorphic: true
# Mirror Hacktivity's KA_CODES for consistency
KA_CODES = {
'IC' => 'Introduction to CyBOK',
'FM' => 'Formal Methods',
'RMG' => 'Risk Management & Governance',
'LR' => 'Law & Regulation',
'HF' => 'Human Factors',
'POR' => 'Privacy & Online Rights',
'MAT' => 'Malware & Attack Technologies',
'AB' => 'Adversarial Behaviours',
'SOIM' => 'Security Operations & Incident Management',
'F' => 'Forensics',
'C' => 'Cryptography',
'AC' => 'Applied Cryptography',
'OSV' => 'Operating Systems & Virtualisation Security',
'DSS' => 'Distributed Systems Security',
'AAA' => 'Authentication, Authorisation and Accountability',
'SS' => 'Software Security',
'WAM' => 'Web & Mobile Security',
'SSL' => 'Secure Software Lifecycle',
'NS' => 'Network Security',
'HS' => 'Hardware Security',
'CPS' => 'Cyber Physical Systems',
'PLT' => 'Physical Layer and Telecommunications Security'
}.freeze
CATEGORY_MAPPING = {
'Introductory Concepts' => ['IC'],
'Human, Organisational & Regulatory Aspects' => %w[RMG LR HF POR],
'Attacks & Defences' => %w[MAT AB SOIM F],
'Systems Security' => %w[C OSV DSS AAA FM],
'Software and Platform Security' => %w[SS WAM SSL],
'Infrastructure Security' => %w[AC NS HS CPS PLT]
}.freeze
def ka_full_name
KA_CODES[ka] || 'Unknown KA'
end
def ka_category
CATEGORY_MAPPING.each do |category, kas|
return category if kas.include?(ka)
end
'Unknown Category'
end
# Parse keywords string back to array (matches Hacktivity behavior)
def keywords_array
return [] if keywords.blank?
# Handle both array-coerced strings and plain comma-separated
keywords.gsub(/[\[\]"]/, '').split(',').map(&:strip)
end
end
end

View File

@@ -4,17 +4,63 @@ module BreakEscape
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
def self.hacktivity_mode?
defined?(::Cybok)
end
# Generate scenario data via ERB
def generate_scenario_data
template_path = scenario_path.join('scenario.json.erb')

View File

@@ -0,0 +1,88 @@
# frozen_string_literal: true
module BreakEscape
# Service to sync CyBOK data from mission.json to database tables.
# Writes to both BreakEscape and Hacktivity tables when Hacktivity is present.
class CybokSyncService
class << self
# Sync CyBOK data for a mission from parsed JSON data
# @param mission [BreakEscape::Mission] the mission to sync
# @param cybok_data [Array, nil] array of CyBOK entries from mission.json
def sync_for_mission(mission, cybok_data)
return 0 if cybok_data.blank?
# Normalize input (handle both array and hash formats)
cybok_entries = Array.wrap(cybok_data)
# Clear existing entries from BreakEscape table
mission.break_escape_cyboks.destroy_all
# Clear Hacktivity entries if available
clear_hacktivity_cyboks(mission) if hacktivity_mode?
count = 0
cybok_entries.each do |entry|
ka = entry['ka'] || entry[:ka]
topic = entry['topic'] || entry[:topic]
keywords = entry['keywords'] || entry[:keywords]
# Serialize keywords array to string (Hacktivity format)
keywords_str = serialize_keywords(keywords)
# Always write to BreakEscape table
mission.break_escape_cyboks.create!(
ka: ka,
topic: topic,
keywords: keywords_str
)
# Also write to Hacktivity table if available
create_hacktivity_cybok(mission, ka, topic, keywords_str) if hacktivity_mode?
count += 1
end
count
end
# Check if Hacktivity mode is active (::Cybok constant exists)
def hacktivity_mode?
defined?(::Cybok)
end
private
def serialize_keywords(keywords)
case keywords
when Array
keywords.join(', ')
when String
keywords
else
keywords.to_s
end
end
def clear_hacktivity_cyboks(mission)
return unless hacktivity_mode?
::Cybok.where(
cybokable_type: 'BreakEscape::Mission',
cybokable_id: mission.id
).destroy_all
end
def create_hacktivity_cybok(mission, ka, topic, keywords_str)
return unless hacktivity_mode?
::Cybok.create!(
cybokable_type: 'BreakEscape::Mission',
cybokable_id: mission.id,
ka: ka,
topic: topic,
keywords: keywords_str
)
end
end
end
end

View File

@@ -4,6 +4,10 @@
<title>BreakEscape - Select Mission</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "break_escape/application", media: "all" %>
<!-- Tippy.js for tooltips -->
<script src="https://unpkg.com/@popperjs/core@2"></script>
<script src="https://unpkg.com/tippy.js@6"></script>
<style>
body {
font-family: Arial, sans-serif;
@@ -17,6 +21,25 @@
text-align: center;
color: #00ff00;
}
.collection-filter {
text-align: center;
margin-bottom: 20px;
}
.collection-filter a {
display: inline-block;
padding: 8px 16px;
margin: 4px;
background: #2a2a2a;
color: #00ff00;
text-decoration: none;
border: 1px solid #00ff00;
border-radius: 4px;
}
.collection-filter a:hover,
.collection-filter a.active {
background: #00ff00;
color: #000;
}
.missions {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
@@ -31,6 +54,7 @@
text-decoration: none;
color: #fff;
transition: transform 0.2s, box-shadow 0.2s;
display: block;
}
.mission-card:hover {
transform: translateY(-5px);
@@ -47,6 +71,12 @@
line-height: 1.6;
margin-bottom: 15px;
}
.mission-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.mission-difficulty {
display: inline-block;
background: #00ff00;
@@ -56,11 +86,30 @@
font-size: 12px;
font-weight: bold;
}
.mission-collection {
display: inline-block;
background: #3b82f6;
color: #fff;
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
</style>
</head>
<body>
<h1>🔓 BreakEscape - Select Your Mission</h1>
<% if BreakEscape::Mission.collections.length > 1 %>
<div class="collection-filter">
<%= link_to "All", missions_path, class: params[:collection].blank? ? 'active' : '' %>
<% BreakEscape::Mission.collections.each do |collection| %>
<%= link_to collection.titleize, missions_path(collection: collection),
class: params[:collection] == collection ? 'active' : '' %>
<% end %>
</div>
<% end %>
<div class="missions">
<% @missions.each do |mission| %>
<%= link_to mission_path(mission), class: 'mission-card' do %>
@@ -68,11 +117,33 @@
<div class="mission-description">
<%= mission.description || "An exciting escape room challenge awaits..." %>
</div>
<div class="mission-difficulty">
Difficulty: <%= "⭐" * mission.difficulty_level %>
<div class="mission-meta">
<span class="mission-difficulty">
Difficulty: <%= "⭐" * mission.difficulty_level %>
</span>
<% if mission.collection.present? && mission.collection != 'default' %>
<span class="mission-collection">
<%= mission.collection.titleize %>
</span>
<% end %>
<%= render partial: 'break_escape/shared/cybok_label',
locals: { cyboks: mission.break_escape_cyboks } %>
</div>
<% end %>
<% end %>
</div>
<script>
// Initialize Tippy.js tooltips
document.addEventListener('DOMContentLoaded', function() {
tippy('[data-tippy-content]', {
theme: 'break-escape',
allowHTML: true,
placement: 'top',
arrow: true
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,25 @@
<% if cyboks.any? %>
<% grouped_cyboks = cyboks.group_by(&:ka).sort.to_h %>
<% all_ka_values = [] %>
<% all_tippy_content = "#{image_tag 'break_escape/cybok_logo_white.svg', style: 'max-height: 20px'} " %>
<% grouped_cyboks.each do |ka, ka_cyboks| %>
<% all_ka_values << ka %>
<% tippy_content = "" %>
<% ka_cyboks.group_by(&:topic).each do |topic, topic_cyboks| %>
<% unique_keywords = topic_cyboks.flat_map { |c| c.keywords_array }.uniq.map(&:downcase).join(', ') %>
<% tippy_content += "<i>#{topic.titlecase}:</i> #{unique_keywords}<br/>" %>
<% end %>
<% all_tippy_content += "<b>#{ka_cyboks.first.ka_full_name} (#{ka}):</b><br/> #{tippy_content}" %>
<% end %>
<%= render partial: 'break_escape/shared/label', locals: {
label_class: 'cybok',
tippy_content: all_tippy_content,
icon_class: defined?(icon_class) ? icon_class : nil,
label_text: "CyBOK: #{all_ka_values.uniq.join(', ')}"
} %>
<% end %>

View File

@@ -0,0 +1,7 @@
<% random_id = generate_random_id %>
<span id="label_<%= random_id %>" class="label label-<%= label_class %>" data-tippy-content="<%= tippy_content %>">
<% if defined?(icon_class) && icon_class.present? %>
<i class="<%= icon_class %>"></i>
<% end %>
<%= label_text %>
</span>

View File

@@ -0,0 +1,10 @@
# frozen_string_literal: true
class AddMetadataToBreakEscapeMissions < ActiveRecord::Migration[7.0]
def change
add_column :break_escape_missions, :secgen_scenario, :string
add_column :break_escape_missions, :collection, :string, default: 'default'
add_index :break_escape_missions, :collection
end
end

View File

@@ -0,0 +1,19 @@
# frozen_string_literal: true
class CreateBreakEscapeCyboks < ActiveRecord::Migration[7.0]
def change
create_table :break_escape_cyboks do |t|
t.string :ka # Knowledge Area code (e.g., "AC", "F", "WAM")
t.string :topic # Topic within the KA
t.string :keywords # Keywords as comma-separated string (matches Hacktivity)
t.string :cybokable_type # Polymorphic type
t.integer :cybokable_id # Polymorphic ID
t.timestamps
end
add_index :break_escape_cyboks, :cybokable_id
add_index :break_escape_cyboks, %i[cybokable_type cybokable_id]
add_index :break_escape_cyboks, :ka
end
end

View File

@@ -1,25 +1,110 @@
puts "Creating BreakEscape missions..."
# frozen_string_literal: true
puts 'Creating/Updating BreakEscape missions...'
# Directories to skip (not actual playable scenarios)
SKIP_DIRS = %w[common compiled ink].freeze
# Infer collection from scenario name for test/demo scenarios
def infer_collection(scenario_name)
return 'testing' if scenario_name.start_with?('test', 'npc-', 'scenario')
return 'testing' if scenario_name.include?('demo') || scenario_name.include?('test')
'default'
end
# Apply default metadata when mission.json is missing
def apply_default_metadata(mission, scenario_name)
mission.display_name = scenario_name.titleize if mission.display_name.blank?
mission.description = "Play the #{scenario_name.titleize} scenario" if mission.description.blank?
mission.difficulty_level = 3 if mission.difficulty_level.blank? || mission.difficulty_level.zero?
mission.collection = infer_collection(scenario_name) if mission.collection.blank?
mission.published = true
mission
end
# List all scenario directories
scenario_dirs = Dir.glob(BreakEscape::Engine.root.join('scenarios/*')).select { |f| File.directory?(f) }
created_count = 0
updated_count = 0
skipped_count = 0
cybok_total = 0
scenario_dirs.each do |dir|
scenario_name = File.basename(dir)
next if scenario_name == 'common' # Skip common directory if it exists
next if SKIP_DIRS.include?(scenario_name)
# Check for scenario.json.erb (required for valid mission)
scenario_template = File.join(dir, 'scenario.json.erb')
unless File.exist?(scenario_template)
puts " ⊘ Skipped: #{scenario_name} (no scenario.json.erb)"
skipped_count += 1
next
end
# Create mission metadata
mission = BreakEscape::Mission.find_or_initialize_by(name: scenario_name)
is_new = mission.new_record?
mission_json_path = File.join(dir, 'mission.json')
if mission.new_record?
mission.display_name = scenario_name.titleize
mission.description = "Play the #{scenario_name.titleize} scenario"
mission.published = true
mission.difficulty_level = 3 # Default, can be updated later
mission.save!
puts " ✓ Created: #{mission.display_name}"
if File.exist?(mission_json_path)
# Load metadata from mission.json
begin
metadata = JSON.parse(File.read(mission_json_path))
mission.display_name = metadata['display_name'] || scenario_name.titleize
mission.description = metadata['description'] || "Play the #{scenario_name.titleize} scenario"
mission.difficulty_level = metadata['difficulty_level'] || 3
mission.secgen_scenario = metadata['secgen_scenario']
mission.collection = metadata['collection'] || 'default'
mission.published = true
if mission.save
# Sync CyBOK data
if metadata['cybok'].present?
cybok_count = BreakEscape::CybokSyncService.sync_for_mission(mission, metadata['cybok'])
cybok_total += cybok_count
puts "#{is_new ? 'Created' : 'Updated'}: #{mission.display_name} (#{cybok_count} CyBOK entries)"
else
puts "#{is_new ? 'Created' : 'Updated'}: #{mission.display_name}"
end
is_new ? created_count += 1 : updated_count += 1
else
puts " ✗ Failed: #{scenario_name} - #{mission.errors.full_messages.join(', ')}"
end
rescue JSON::ParserError => e
puts " ⚠ Invalid mission.json for #{scenario_name}: #{e.message}"
# Fall back to defaults
apply_default_metadata(mission, scenario_name)
if mission.save
puts "#{is_new ? 'Created' : 'Updated'} (defaults): #{mission.display_name}"
is_new ? created_count += 1 : updated_count += 1
else
puts " ✗ Failed: #{scenario_name} - #{mission.errors.full_messages.join(', ')}"
end
end
else
puts " - Exists: #{mission.display_name}"
# No mission.json - use defaults
apply_default_metadata(mission, scenario_name)
if mission.save
puts "#{is_new ? 'Created' : 'Updated'} (defaults): #{mission.display_name}"
is_new ? created_count += 1 : updated_count += 1
else
puts " ✗ Failed: #{scenario_name} - #{mission.errors.full_messages.join(', ')}"
end
end
end
puts "Done! Created #{BreakEscape::Mission.count} missions."
puts ''
puts '=' * 50
puts "Done! #{BreakEscape::Mission.count} missions total."
puts " Created: #{created_count}, Updated: #{updated_count}, Skipped: #{skipped_count}"
puts " CyBOK entries synced: #{cybok_total}"
puts " Collections: #{BreakEscape::Mission.collections.join(', ')}"
if BreakEscape::CybokSyncService.hacktivity_mode?
puts ' Mode: Hacktivity (CyBOK data synced to both tables)'
else
puts ' Mode: Standalone (CyBOK data in break_escape_cyboks only)'
end
puts '=' * 50

View File

@@ -541,6 +541,38 @@ If issues arise:
---
## View Layer Implementation
### CyBOK Label Partial
**File**: `app/views/break_escape/shared/_cybok_label.html.erb`
Mirrors Hacktivity's `_cybok_label.html.erb` partial:
- Groups CyBOK entries by Knowledge Area (KA)
- Builds Tippy.js tooltip content with topics and keywords
- Uses the shared `_label.html.erb` partial for rendering
### Label Partial
**File**: `app/views/break_escape/shared/_label.html.erb`
Generic label component with:
- Random ID generation for DOM uniqueness
- Tippy.js `data-tippy-content` attribute
- Icon support (optional)
### Stylesheets
- `app/assets/stylesheets/break_escape/labels.css` - Label component styles
- `app/assets/stylesheets/break_escape/tooltips.css` - Tippy.js theme matching Hacktivity
### Helper Methods
**File**: `app/helpers/break_escape/application_helper.rb`
- `generate_random_id` - Creates unique DOM IDs using SecureRandom
### Assets
- `app/assets/images/break_escape/cybok_logo_white.svg` - CyBOK logo for tooltips
---
## Future Enhancements
1. **Admin UI**: Add mission metadata editing in admin panel

View File

@@ -0,0 +1,29 @@
{
"display_name": "Biometric Breach",
"description": "Investigate a security breach at a high-security research facility. Use biometric forensics tools to identify the intruder, track their movements through the facility, and recover stolen research data before it leaves the building.",
"difficulty_level": 3,
"secgen_scenario": null,
"collection": "security_investigations",
"cybok": [
{
"ka": "AAA",
"topic": "Authentication",
"keywords": ["Biometric authentication", "Fingerprint analysis", "Identity verification", "Multi-factor authentication"]
},
{
"ka": "F",
"topic": "Artifact Analysis",
"keywords": ["Digital forensics", "Evidence collection", "Fingerprint forensics"]
},
{
"ka": "SOIM",
"topic": "Security Operations & Incident Management",
"keywords": ["Incident response", "Security monitoring", "Access control investigation"]
},
{
"ka": "HF",
"topic": "Human Factors",
"keywords": ["Physical security bypass", "Social engineering awareness"]
}
]
}

View File

@@ -0,0 +1,29 @@
{
"display_name": "CEO Exfiltration",
"description": "A corporate espionage scenario where you must navigate executive offices to extract sensitive data. Test your skills in data exfiltration and covert operations while avoiding detection.",
"difficulty_level": 4,
"secgen_scenario": null,
"collection": "data_exfiltration",
"cybok": [
{
"ka": "AB",
"topic": "Adversarial Behaviours",
"keywords": ["Corporate espionage", "Data theft", "Covert operations", "Insider threat"]
},
{
"ka": "MAT",
"topic": "Malware & Attack Technologies",
"keywords": ["Data exfiltration techniques", "Covert channels"]
},
{
"ka": "F",
"topic": "Forensics",
"keywords": ["Anti-forensics", "Evidence handling", "Data recovery"]
},
{
"ka": "HF",
"topic": "Human Factors",
"keywords": ["Physical security", "Access control bypass"]
}
]
}

View File

@@ -0,0 +1,24 @@
{
"display_name": "CyBOK Heist",
"description": "Recover the Professor's backup of the CyBOK LaTeX source files. Navigate through the department offices, solve puzzles, pick locks, and crack the safe containing the precious backup HDD.",
"difficulty_level": 2,
"secgen_scenario": null,
"collection": "physical_security",
"cybok": [
{
"ka": "HF",
"topic": "Human Factors",
"keywords": ["Physical security", "Lock bypass", "Security awareness", "Social engineering"]
},
{
"ka": "C",
"topic": "Cryptography",
"keywords": ["Safe combinations", "Code breaking", "Cipher fundamentals"]
},
{
"ka": "AC",
"topic": "Applied Cryptography",
"keywords": ["Encoding", "Cipher solving", "Cryptanalysis basics"]
}
]
}

View File

@@ -0,0 +1,14 @@
{
"display_name": "NPC Hub Demo - Ghost Protocol",
"description": "Demonstration scenario for NPC hub interactions and the Ghost Protocol narrative.",
"difficulty_level": 2,
"secgen_scenario": null,
"collection": "testing",
"cybok": [
{
"ka": "HF",
"topic": "Human Factors",
"keywords": ["NPC interaction", "Social engineering", "Dialogue systems"]
}
]
}

View File

@@ -0,0 +1,14 @@
{
"display_name": "NPC Patrol Lockpick",
"description": "Test scenario demonstrating NPC patrol mechanics combined with lockpicking gameplay.",
"difficulty_level": 2,
"secgen_scenario": null,
"collection": "testing",
"cybok": [
{
"ka": "HF",
"topic": "Human Factors",
"keywords": ["Physical security", "Lock bypass", "Patrol avoidance"]
}
]
}

View File

@@ -0,0 +1,7 @@
{
"display_name": "NPC Sprite Test 2",
"description": "Technical test scenario for NPC sprite rendering and animations.",
"difficulty_level": 1,
"secgen_scenario": null,
"collection": "testing"
}

View File

@@ -0,0 +1,7 @@
{
"display_name": "NPC Sprite Test 3",
"description": "Technical test scenario for advanced NPC sprite behaviours.",
"difficulty_level": 1,
"secgen_scenario": null,
"collection": "testing"
}

View File

@@ -0,0 +1,7 @@
{
"display_name": "Scenario 1",
"description": "A basic introductory scenario demonstrating core gameplay mechanics.",
"difficulty_level": 1,
"secgen_scenario": null,
"collection": "testing"
}

View File

@@ -0,0 +1,7 @@
{
"display_name": "Scenario 2",
"description": "A test scenario for gameplay features and room navigation.",
"difficulty_level": 1,
"secgen_scenario": null,
"collection": "testing"
}

View File

@@ -0,0 +1,7 @@
{
"display_name": "Scenario 3",
"description": "A test scenario for advanced gameplay mechanics.",
"difficulty_level": 2,
"secgen_scenario": null,
"collection": "testing"
}

View File

@@ -0,0 +1,7 @@
{
"display_name": "Scenario 4",
"description": "A test scenario for complex gameplay interactions.",
"difficulty_level": 2,
"secgen_scenario": null,
"collection": "testing"
}

View File

@@ -0,0 +1,7 @@
{
"display_name": "Multi-room NPC Test",
"description": "Technical test for NPCs navigating between multiple rooms.",
"difficulty_level": 1,
"secgen_scenario": null,
"collection": "testing"
}

View File

@@ -0,0 +1,7 @@
{
"display_name": "NPC Face Player Test",
"description": "Technical test for NPC facing player mechanic.",
"difficulty_level": 1,
"secgen_scenario": null,
"collection": "testing"
}

View File

@@ -0,0 +1,7 @@
{
"display_name": "NPC Patrol Test",
"description": "Technical test for NPC patrol route system.",
"difficulty_level": 1,
"secgen_scenario": null,
"collection": "testing"
}

View File

@@ -0,0 +1,7 @@
{
"display_name": "NPC Personal Space Test",
"description": "Technical test for NPC personal space and collision avoidance.",
"difficulty_level": 1,
"secgen_scenario": null,
"collection": "testing"
}

View File

@@ -0,0 +1,7 @@
{
"display_name": "NPC Waypoints Test",
"description": "Technical test for NPC waypoint navigation system.",
"difficulty_level": 1,
"secgen_scenario": null,
"collection": "testing"
}

View File

@@ -0,0 +1,19 @@
{
"display_name": "RFID Multiprotocol Test",
"description": "Test scenario demonstrating multiple RFID protocol types and cloning mechanics.",
"difficulty_level": 2,
"secgen_scenario": null,
"collection": "testing",
"cybok": [
{
"ka": "AAA",
"topic": "Authentication",
"keywords": ["RFID authentication", "Access control", "Multi-protocol systems"]
},
{
"ka": "HS",
"topic": "Hardware Security",
"keywords": ["RFID protocols", "Card cloning", "Proximity cards"]
}
]
}

View File

@@ -0,0 +1,19 @@
{
"display_name": "RFID Test",
"description": "Test scenario for RFID card mechanics and access control.",
"difficulty_level": 1,
"secgen_scenario": null,
"collection": "testing",
"cybok": [
{
"ka": "AAA",
"topic": "Authentication",
"keywords": ["RFID authentication", "Access control", "Physical tokens"]
},
{
"ka": "HS",
"topic": "Hardware Security",
"keywords": ["RFID technology", "Smart cards"]
}
]
}

View File

@@ -0,0 +1,7 @@
{
"display_name": "Complex Multidirection Test",
"description": "Technical test for complex multi-directional room navigation.",
"difficulty_level": 1,
"secgen_scenario": null,
"collection": "testing"
}

View File

@@ -0,0 +1,7 @@
{
"display_name": "Horizontal Layout Test",
"description": "Technical test for horizontally-oriented room layouts.",
"difficulty_level": 1,
"secgen_scenario": null,
"collection": "testing"
}

View File

@@ -0,0 +1,7 @@
{
"display_name": "Mixed Room Sizes Test",
"description": "Technical test for scenarios with varying room dimensions.",
"difficulty_level": 1,
"secgen_scenario": null,
"collection": "testing"
}

View File

@@ -0,0 +1,7 @@
{
"display_name": "Multiple Connections Test",
"description": "Technical test for rooms with multiple door connections.",
"difficulty_level": 1,
"secgen_scenario": null,
"collection": "testing"
}

View File

@@ -0,0 +1,7 @@
{
"display_name": "Vertical Layout Test",
"description": "Technical test for vertically-oriented room layouts.",
"difficulty_level": 1,
"secgen_scenario": null,
"collection": "testing"
}

View File

@@ -0,0 +1,7 @@
{
"display_name": "Timed Messages Example",
"description": "Example scenario demonstrating timed message delivery mechanics.",
"difficulty_level": 1,
"secgen_scenario": null,
"collection": "testing"
}

View File

@@ -0,0 +1,7 @@
{
"display_name": "Title Screen Demo",
"description": "Demonstration of the title screen and menu system.",
"difficulty_level": 1,
"secgen_scenario": null,
"collection": "testing"
}

View File

@@ -10,7 +10,20 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2025_11_20_160000) do
ActiveRecord::Schema[7.2].define(version: 2025_11_25_000002) do
create_table "break_escape_cyboks", force: :cascade do |t|
t.string "ka"
t.string "topic"
t.string "keywords"
t.string "cybokable_type"
t.integer "cybokable_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["cybokable_id"], name: "index_break_escape_cyboks_on_cybokable_id"
t.index ["cybokable_type", "cybokable_id"], name: "index_break_escape_cyboks_on_cybokable_type_and_cybokable_id"
t.index ["ka"], name: "index_break_escape_cyboks_on_ka"
end
create_table "break_escape_demo_users", force: :cascade do |t|
t.string "handle", null: false
t.string "role", default: "user", null: false
@@ -45,6 +58,9 @@ ActiveRecord::Schema[7.2].define(version: 2025_11_20_160000) do
t.integer "difficulty_level", default: 1, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "secgen_scenario"
t.string "collection", default: "default"
t.index ["collection"], name: "index_break_escape_missions_on_collection"
t.index ["name"], name: "index_break_escape_missions_on_name", unique: true
t.index ["published"], name: "index_break_escape_missions_on_published"
end