initial commit
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user