initial commit

This commit is contained in:
Marius Rometsch
2026-02-05 22:42:30 +01:00
commit 0068785924
66 changed files with 13795 additions and 0 deletions
@@ -0,0 +1,230 @@
/**
* Base class for all lesson modules
* All lesson modules should extend this class
*/
class LessonModule {
constructor(config) {
this.config = config;
this.lessonKey = config.lessonKey;
}
/**
* Validate an answer for a specific question
* @param {string} questionId - The question identifier
* @param {any} answer - The participant's answer
* @returns {Object} { isCorrect, pointsAwarded, feedback }
*/
async validateAnswer(questionId, answer) {
const step = this.config.steps.find(s => s.id === questionId);
if (!step || step.type !== 'question') {
throw new Error(`Question ${questionId} not found`);
}
return this._validateQuestionType(step, answer);
}
/**
* Internal validation based on question type
*/
_validateQuestionType(step, answer) {
switch (step.questionType) {
case 'single_choice':
return this._validateSingleChoice(step, answer);
case 'multiple_choice':
return this._validateMultipleChoice(step, answer);
case 'free_text':
return this._validateFreeText(step, answer);
default:
throw new Error(`Unknown question type: ${step.questionType}`);
}
}
_validateSingleChoice(step, answer) {
const selectedOption = step.options.find(opt => opt.id === answer);
if (!selectedOption) {
return {
isCorrect: false,
pointsAwarded: 0,
feedback: step.feedback?.incorrect || 'Incorrect answer'
};
}
return {
isCorrect: selectedOption.isCorrect,
pointsAwarded: selectedOption.isCorrect ? selectedOption.points : 0,
feedback: selectedOption.isCorrect
? (step.feedback?.correct || 'Correct!')
: (step.feedback?.incorrect || 'Incorrect answer')
};
}
_validateMultipleChoice(step, answers) {
// answers should be an array of option IDs
if (!Array.isArray(answers)) {
return {
isCorrect: false,
pointsAwarded: 0,
feedback: step.feedback?.incorrect || 'Invalid answer format'
};
}
const correctOptions = step.options.filter(opt => opt.isCorrect).map(opt => opt.id);
const selectedCorrect = answers.filter(a => correctOptions.includes(a));
const selectedIncorrect = answers.filter(a => !correctOptions.includes(a));
// Calculate points
const pointsAwarded = selectedCorrect.reduce((sum, id) => {
const option = step.options.find(opt => opt.id === id);
return sum + (option?.points || 0);
}, 0);
const isFullyCorrect = selectedCorrect.length === correctOptions.length &&
selectedIncorrect.length === 0;
const isPartiallyCorrect = selectedCorrect.length > 0 && !isFullyCorrect;
let feedback = step.feedback?.incorrect || 'Incorrect answer';
if (isFullyCorrect) {
feedback = step.feedback?.correct || 'Correct!';
} else if (isPartiallyCorrect) {
feedback = step.feedback?.partial || step.feedback?.correct || 'Partially correct';
}
return {
isCorrect: isFullyCorrect,
isPartial: isPartiallyCorrect,
pointsAwarded,
feedback
};
}
_validateFreeText(step, answer) {
if (!answer || typeof answer !== 'string') {
return {
isCorrect: false,
pointsAwarded: 0,
feedback: step.feedback?.incorrect || 'Answer is required'
};
}
if (!step.validationRules || step.validationRules.length === 0) {
// No validation rules, accept any non-empty answer
const points = answer.trim().length > 0 ? step.maxPoints : 0;
return {
isCorrect: points > 0,
pointsAwarded: points,
feedback: points > 0
? (step.feedback?.correct || 'Answer received')
: (step.feedback?.incorrect || 'Answer is too short')
};
}
let passedRules = 0;
const totalRules = step.validationRules.length;
for (const rule of step.validationRules) {
if (this._checkValidationRule(rule, answer)) {
passedRules++;
}
}
const scorePercentage = passedRules / totalRules;
const pointsAwarded = Math.round(step.maxPoints * scorePercentage);
const isCorrect = scorePercentage >= 0.7; // 70% threshold
return {
isCorrect,
pointsAwarded,
feedback: isCorrect
? (step.feedback?.correct || 'Good answer!')
: (step.feedback?.incorrect || 'Please review your answer')
};
}
_checkValidationRule(rule, answer) {
const lowerAnswer = (answer || '').toLowerCase();
switch (rule.type) {
case 'contains_keywords':
const matches = rule.keywords.filter(keyword =>
lowerAnswer.includes(keyword.toLowerCase())
).length;
return matches >= (rule.minMatches || 1);
case 'min_length':
return answer.length >= rule.value;
case 'max_length':
return answer.length <= rule.value;
case 'regex':
return new RegExp(rule.pattern, rule.flags || 'i').test(answer);
default:
return false;
}
}
/**
* Get interactive component data for a step
* Can be overridden by subclasses for dynamic content
*/
async getInteractiveData(stepId) {
const step = this.config.steps.find(s => s.id === stepId);
if (!step || step.type !== 'interactive') {
throw new Error(`Interactive step ${stepId} not found`);
}
return {
component: step.interactiveComponent,
props: step.componentProps || {}
};
}
/**
* Get lesson content for rendering (without answers)
*/
getContent() {
return {
lessonKey: this.lessonKey,
title: this.config.title,
description: this.config.description,
difficultyLevel: this.config.difficultyLevel,
estimatedDuration: this.config.estimatedDuration,
steps: this.config.steps.map(step => ({
id: step.id,
type: step.type,
title: step.title,
content: step.content,
// For question steps, don't send correct answers
...(step.type === 'question' && {
questionType: step.questionType,
question: step.question,
maxPoints: step.maxPoints,
options: step.options?.map(opt => ({
id: opt.id,
text: opt.text
// isCorrect and points are intentionally omitted
}))
}),
// For interactive steps, send component info
...(step.type === 'interactive' && {
interactiveComponent: step.interactiveComponent,
componentProps: step.componentProps
})
})),
scoring: {
maxTotalPoints: this.config.scoring?.maxTotalPoints || 100,
passingScore: this.config.scoring?.passingScore || 70
}
};
}
/**
* Get full configuration (for debugging/admin)
*/
getFullConfig() {
return this.config;
}
}
module.exports = LessonModule;
@@ -0,0 +1,83 @@
const LessonModule = require('../base/LessonModule');
class BrowserInBrowserLesson extends LessonModule {
constructor(config) {
super(config);
}
// Get interactive data for the BitB demo
getInteractiveData(stepId) {
if (stepId === 'bitb-demo') {
return {
scenarios: [
{
id: 'legitimate',
title: 'Legitimate OAuth Popup',
provider: 'Google',
domain: 'accounts.google.com',
isReal: true,
description: 'This simulates how a REAL browser popup would behave',
indicators: [
'Can be dragged outside browser window',
'Has native window controls',
'Address bar text is not selectable (real browser UI)',
'Right-click shows browser context menu, not page menu',
'Appears as separate window in system taskbar'
]
},
{
id: 'bitb-attack',
title: 'Browser-in-the-Browser Attack',
provider: 'Microsoft',
domain: 'login.microsoftonline.com',
isReal: false,
description: 'This is a FAKE popup window created with HTML/CSS/JavaScript',
indicators: [
'Cannot be dragged outside the main browser window',
'Entire window is trapped within the page boundaries',
'Address bar is just HTML text/image (right-click shows Inspect)',
'Window controls (minimize, maximize, close) are fake buttons',
'Does not appear in system taskbar as separate window'
]
}
],
testInstructions: [
'Try to drag each popup window outside the main browser area',
'Right-click on the address bar to see if you can inspect it as HTML',
'Look for subtle differences in fonts, spacing, or shadows',
'Check if the window controls behave like real browser buttons',
'Notice if the popup can extend beyond the main window boundaries'
],
realWorldExamples: [
{
year: 2022,
target: 'Corporate employees',
provider: 'Microsoft OAuth',
description: 'Attackers used BitB to steal enterprise credentials'
},
{
year: 2022,
target: 'Cryptocurrency users',
provider: 'Google Sign-in',
description: 'Fake crypto platforms used BitB for account takeovers'
},
{
year: 2023,
target: 'GitHub developers',
provider: 'GitHub OAuth',
description: 'Malicious sites mimicked GitHub login to steal tokens'
}
]
};
}
return null;
}
// Validate specific BitB detection knowledge
async validateAnswer(questionId, answer) {
// Use base class validation for standard question types
return super.validateAnswer(questionId, answer);
}
}
module.exports = BrowserInBrowserLesson;
@@ -0,0 +1,16 @@
const LessonModule = require('../base/LessonModule');
/**
* Phishing Email Detection Basics Lesson
* Teaches participants to identify common phishing tactics
*/
class PhishingEmailBasicsLesson extends LessonModule {
constructor(config) {
super(config);
}
// This lesson uses the default validation from the base class
// No custom validation needed for this beginner lesson
}
module.exports = PhishingEmailBasicsLesson;
@@ -0,0 +1,209 @@
const LessonModule = require('../base/LessonModule');
class SQLInjectionShopLesson extends LessonModule {
constructor(config) {
super(config);
}
// Mock database with products
getMockDatabase() {
return {
products: [
{ id: 1, name: 'Laptop Pro 15', price: 1299.99, category: 'Electronics', stock: 15 },
{ id: 2, name: 'Wireless Mouse', price: 29.99, category: 'Accessories', stock: 50 },
{ id: 3, name: 'USB-C Cable', price: 12.99, category: 'Accessories', stock: 100 },
{ id: 4, name: 'Gaming Keyboard', price: 89.99, category: 'Electronics', stock: 25 },
{ id: 5, name: 'Monitor 27"', price: 349.99, category: 'Electronics', stock: 20 },
{ id: 6, name: 'Webcam HD', price: 79.99, category: 'Electronics', stock: 30 },
{ id: 7, name: 'Desk Lamp', price: 34.99, category: 'Office', stock: 40 },
{ id: 8, name: 'Notebook Set', price: 15.99, category: 'Office', stock: 60 }
],
users: [
{ id: 1, username: 'admin', password: 'hashed_admin_password', role: 'admin' },
{ id: 2, username: 'john_doe', password: 'hashed_user_password', role: 'customer' },
{ id: 3, username: 'jane_smith', password: 'hashed_user_password', role: 'customer' }
],
orders: [
{ id: 1, user_id: 2, total: 1329.98, status: 'shipped' },
{ id: 2, user_id: 3, total: 89.99, status: 'processing' }
]
};
}
// Simulate vulnerable SQL query
executeVulnerableQuery(searchTerm) {
const db = this.getMockDatabase();
// Build the "vulnerable" query string for educational display
const vulnerableQuery = `SELECT * FROM products WHERE name LIKE '%${searchTerm}%'`;
// Detect SQL injection attempts
const injectionDetected = this.detectInjection(searchTerm);
let results = [];
let injectionType = null;
let explanation = '';
if (injectionDetected) {
const injectionInfo = this.analyzeInjection(searchTerm);
injectionType = injectionInfo.type;
explanation = injectionInfo.explanation;
// Simulate different injection results
if (injectionInfo.type === 'OR_ALWAYS_TRUE') {
// Return all products (simulating OR '1'='1')
results = db.products;
} else if (injectionInfo.type === 'UNION_SELECT') {
// Simulate UNION attack showing user data
results = [
{ id: 'INJECTED', name: 'admin', price: 'hashed_admin_password', category: 'LEAKED DATA', stock: 'admin' },
{ id: 'INJECTED', name: 'john_doe', price: 'hashed_user_password', category: 'LEAKED DATA', stock: 'customer' },
{ id: 'INJECTED', name: 'jane_smith', price: 'hashed_user_password', category: 'LEAKED DATA', stock: 'customer' }
];
} else if (injectionInfo.type === 'DROP_TABLE') {
// Simulate destructive command
results = [];
explanation += ' In a real scenario, this could delete the entire products table!';
} else if (injectionInfo.type === 'COMMENT_INJECTION') {
// Bypass rest of query
results = db.products;
}
} else {
// Normal search - filter products by name
results = db.products.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}
return {
query: vulnerableQuery,
results,
injectionDetected,
injectionType,
explanation,
recordCount: results.length
};
}
// Detect if input contains SQL injection
detectInjection(input) {
const injectionPatterns = [
/'/, // Single quote
/--/, // SQL comment
/;/, // Statement separator
/union/i, // UNION keyword
/select/i, // SELECT keyword
/drop/i, // DROP keyword
/insert/i, // INSERT keyword
/update/i, // UPDATE keyword
/delete/i, // DELETE keyword
/or\s+['"]?\d+['"]?\s*=\s*['"]?\d+['"]?/i // OR 1=1 pattern
];
return injectionPatterns.some(pattern => pattern.test(input));
}
// Analyze the type of SQL injection
analyzeInjection(input) {
const lowerInput = input.toLowerCase();
if (lowerInput.includes('union') && lowerInput.includes('select')) {
return {
type: 'UNION_SELECT',
explanation: '⚠️ UNION SELECT injection detected! This technique combines results from multiple tables, potentially exposing sensitive data like usernames and passwords.'
};
}
if (lowerInput.includes('drop')) {
return {
type: 'DROP_TABLE',
explanation: '🚨 DROP TABLE injection detected! This is a destructive attack that could delete entire database tables. Critical data loss would occur!'
};
}
if (lowerInput.includes("'") && (lowerInput.includes('or') || lowerInput.includes('||'))) {
if (lowerInput.match(/or\s+['"]?\d+['"]?\s*=\s*['"]?\d+['"]?/)) {
return {
type: 'OR_ALWAYS_TRUE',
explanation: "⚠️ OR injection detected! The condition '1'='1' is always true, bypassing the intended filter and returning ALL records."
};
}
}
if (lowerInput.includes('--') || lowerInput.includes('#')) {
return {
type: 'COMMENT_INJECTION',
explanation: '⚠️ Comment injection detected! The -- sequence comments out the rest of the SQL query, potentially bypassing security checks.'
};
}
if (lowerInput.includes(';')) {
return {
type: 'MULTIPLE_STATEMENTS',
explanation: '⚠️ Multiple statement injection detected! The semicolon allows execution of additional SQL commands, enabling complex attacks.'
};
}
// Generic injection
return {
type: 'GENERIC',
explanation: '⚠️ SQL injection attempt detected! Special characters in the input could manipulate the query structure.'
};
}
// Demonstrate safe parameterized query
executeSafeQuery(searchTerm) {
const db = this.getMockDatabase();
// Show the safe query with placeholder
const safeQuery = `SELECT * FROM products WHERE name LIKE ?`;
const parameter = `%${searchTerm}%`;
// Execute safe search (treats all input as literal data)
const results = db.products.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
);
return {
query: safeQuery,
parameter,
results,
explanation: '✅ Parameterized query used! User input is treated as data only, never as SQL code. Injection is impossible.',
recordCount: results.length
};
}
// Get interactive data for the SQL shop demo
getInteractiveData(stepId) {
if (stepId === 'shop-demo') {
return {
database: this.getMockDatabase(),
examples: [
{
label: 'Normal Search',
input: 'laptop',
description: 'Search for products containing "laptop"'
},
{
label: 'View All Products (OR injection)',
input: "' OR '1'='1",
description: 'Exploit: Returns all products by making condition always true'
},
{
label: 'Extract User Data (UNION)',
input: "' UNION SELECT id, username, password, role, 'LEAKED' FROM users--",
description: 'Exploit: Combines product results with user table data'
},
{
label: 'Destructive Attack (DROP)',
input: "'; DROP TABLE products--",
description: 'Exploit: Attempts to delete the products table'
}
]
};
}
return null;
}
}
module.exports = SQLInjectionShopLesson;