diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e3db262 --- /dev/null +++ b/.env.example @@ -0,0 +1,119 @@ +# ============================================================================= +# DEASI - Environment Configuration Template +# ============================================================================= +# SECURITY WARNING: This is a TEMPLATE file. DO NOT commit actual credentials. +# Copy this to .env and fill in your actual values. +# Add .env to .gitignore to prevent accidental commits. +# ============================================================================= + +# Node Environment +NODE_ENV=production + +# Database Configuration +DB_URI=mongodb+srv://username:password@cluster.mongodb.net/deasi?retryWrites=true&w=majority +DB_NAME=deasi +DB_POOL_SIZE=10 + +# JWT Configuration +JWT_SECRET=your_super_secret_jwt_key_minimum_32_characters_long_here +JWT_EXPIRY=1h +REFRESH_TOKEN_SECRET=your_super_secret_refresh_token_key_minimum_32_characters_long_here +REFRESH_TOKEN_EXPIRY=7d + +# Encryption Configuration +ENCRYPTION_KEY=your_32_character_encryption_key_for_aes_256_gcm_encryption_here + +# API Configuration +API_PORT=3000 +API_HOST=0.0.0.0 +API_VERSION=v1 +API_KEY=your_secure_api_key_here + +# CORS Configuration +ALLOWED_ORIGINS=https://yourdomain.com,https://app.yourdomain.com,http://localhost:3000 + +# Rate Limiting +RATE_LIMIT_WINDOW_MS=900000 +RATE_LIMIT_MAX_REQUESTS=100 +LOGIN_ATTEMPT_MAX=5 +LOGIN_LOCKOUT_TIME_MS=900000 + +# Security +BCRYPT_ROUNDS=12 +SESSION_SECRET=your_session_secret_key_here +SECURE_COOKIE=true +SAME_SITE=strict + +# Logging Configuration +LOG_LEVEL=info +LOG_FORMAT=json +LOG_FILE_PATH=logs/ +LOG_MAX_SIZE=10m +LOG_MAX_FILES=14d + +# Email Configuration (for password reset, notifications) +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USER=your-email@gmail.com +SMTP_PASS=your_app_specific_password_here +SMTP_FROM=noreply@deasi.com + +# Monitoring & Observability +SENTRY_DSN=your_sentry_dsn_url_here_optional +MONITORING_ENABLED=true + +# External Services +BOT_API_KEY=your_bot_api_key_here +BOT_API_ENDPOINT=https://api.bot-service.com + +# SSL/TLS Configuration +SSL_CERT_PATH=/etc/ssl/certs/your-cert.crt +SSL_KEY_PATH=/etc/ssl/private/your-key.key + +# Session Management +SESSION_TIMEOUT_MS=3600000 +SESSION_COOKIE_NAME=deasi_session + +# Feature Flags +FEATURE_MFA_ENABLED=true +FEATURE_AUDIT_LOGGING=true +FEATURE_IP_WHITELIST=false + +# Backup Configuration +BACKUP_ENABLED=true +BACKUP_SCHEDULE=0 2 * * * +BACKUP_RETENTION_DAYS=30 + +# Development (Only for local development, never in production) +DEBUG_MODE=false +DEV_SKIP_AUTH=false + +# ============================================================================= +# INSTRUCTIONS FOR SETTING UP .env +# ============================================================================= +# +# 1. Copy this file: cp .env.example .env +# +# 2. Generate secure secrets: +# - Node: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" +# - Or use: openssl rand -hex 32 +# +# 3. Fill in all values with your actual configuration +# +# 4. Ensure .env is in .gitignore +# +# 5. Never commit the .env file to version control +# +# 6. Use secure methods to share credentials (e.g., 1Password, LastPass, Vault) +# +# 7. Rotate secrets regularly (at least quarterly) +# +# 8. Use different secrets for dev/staging/production +# +# 9. For production, use environment variables from your deployment platform: +# - AWS Secrets Manager +# - Azure Key Vault +# - HashiCorp Vault +# - Google Cloud Secret Manager +# +# ============================================================================= \ No newline at end of file diff --git a/.github/workflows/deno.yml b/.github/workflows/deno.yml new file mode 100644 index 0000000..2d82fc5 --- /dev/null +++ b/.github/workflows/deno.yml @@ -0,0 +1,42 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +# This workflow will install Deno then run `deno lint` and `deno test`. +# For more information see: https://github.com/denoland/setup-deno + +name: Deno + +on: + push: + branches: ["Richie121"] + pull_request: + branches: ["Richy121"] + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Setup repo + uses: actions/checkout@v4 + + - name: Setup Deno + # uses: denoland/setup-deno@v1 + uses: denoland/setup-deno@61fe2df320078202e33d7d5ad347e7dcfa0e8f31 # v1.1.2 + with: + deno-version: v1.x + + # Uncomment this step to verify the use of 'deno fmt' on each commit. + # - name: Verify formatting + # run: deno fmt --check + + - name: Run linter + run: deno lint + + - name: Run tests + run: deno test -A diff --git a/SECURITY_HARDENING_GUIDE.md b/SECURITY_HARDENING_GUIDE.md new file mode 100644 index 0000000..98a39bb --- /dev/null +++ b/SECURITY_HARDENING_GUIDE.md @@ -0,0 +1,375 @@ +# Security Hardening Implementation Guide for DeASI + +## Overview +This document provides comprehensive security hardening recommendations for the DeASI project. + +## 1. DATABASE SECURITY - CRITICAL ISSUES + +### Current Problems: +- ❌ SHA256 used for password hashing (not resistant to attacks) +- ❌ No input validation +- ❌ No rate limiting on login +- ❌ Insecure password requirements +- ❌ No audit logging + +### Fixes Applied: + +#### Replace SHA256 with bcrypt: +```javascript +// BEFORE (INSECURE) +password: crypto.createHash('sha256').update(password).digest('hex') + +// AFTER (SECURE) +const bcrypt = require('bcrypt'); +const hashedPassword = await bcrypt.hash(password, 12); +``` + +#### Add Input Validation: +- Username: minimum 3 characters +- Password: minimum 8 characters +- IP validation for bots +- MongoDB ObjectId validation + +#### Add Password Change Verification: +- Requires old password verification +- Enforces new password complexity + +--- + +## 2. API SECURITY + +### Recommended Middleware: +```javascript +const helmet = require('helmet'); +const rateLimit = require('express-rate-limit'); +const mongoSanitize = require('express-mongo-sanitize'); +const cors = require('cors'); + +// Security headers +app.use(helmet()); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 100 +}); +app.use(limiter); + +// Data sanitization +app.use(mongoSanitize()); + +// CORS configuration +app.use(cors({ + origin: process.env.ALLOWED_ORIGINS?.split(','), + credentials: true +})); +``` + +--- + +## 3. AUTHENTICATION & AUTHORIZATION + +### Implement JWT: +```javascript +const jwt = require('jsonwebtoken'); + +function generateToken(userId) { + return jwt.sign( + { userId }, + process.env.JWT_SECRET, + { expiresIn: '1h' } + ); +} + +function generateRefreshToken(userId) { + return jwt.sign( + { userId }, + process.env.REFRESH_TOKEN_SECRET, + { expiresIn: '7d' } + ); +} +``` + +### Add Role-Based Access Control: +```javascript +const roles = { + ADMIN: 'admin', + USER: 'user', + BOT_MANAGER: 'bot_manager' +}; + +function requireRole(role) { + return (req, res, next) => { + if (req.user.role !== role) { + return res.status(403).json({ error: 'Access denied' }); + } + next(); + }; +} +``` + +--- + +## 4. DATA PROTECTION + +### Encryption at Rest: +```javascript +const crypto = require('crypto'); +const algorithm = 'aes-256-gcm'; + +function encryptField(value, key) { + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv(algorithm, Buffer.from(key), iv); + let encrypted = cipher.update(value, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + const authTag = cipher.getAuthTag(); + return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`; +} + +function decryptField(encrypted, key) { + const [iv, authTag, encryptedValue] = encrypted.split(':'); + const decipher = crypto.createDecipheriv( + algorithm, + Buffer.from(key), + Buffer.from(iv, 'hex') + ); + decipher.setAuthTag(Buffer.from(authTag, 'hex')); + let decrypted = decipher.update(encryptedValue, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + return decrypted; +} +``` + +### TLS Configuration: +```javascript +const https = require('https'); +const fs = require('fs'); + +const options = { + key: fs.readFileSync('path/to/private-key.pem'), + cert: fs.readFileSync('path/to/certificate.pem') +}; + +https.createServer(options, app).listen(443); +``` + +--- + +## 5. CODE & DEPLOYMENT SECURITY + +### ESLint Security Rules: +```json +{ + "extends": ["plugin:security/recommended"], + "plugins": ["security"], + "rules": { + "security/detect-object-injection": "warn", + "security/detect-non-literal-regexp": "warn", + "security/detect-unsafe-regex": "error" + } +} +``` + +### Secrets Management (.env): +``` +DB_URI=mongodb+srv://user:password@cluster.mongodb.net/deasi +JWT_SECRET=your_super_secret_key +REFRESH_TOKEN_SECRET=your_refresh_secret_key +ENCRYPTION_KEY=your_32_char_encryption_key +API_KEY=your_api_key +ALLOWED_ORIGINS=https://yourdomain.com,https://app.yourdomain.com +``` + +### .gitignore: +``` +.env +.env.local +.env.*.local +node_modules/ +dist/ +build/ +*.log +.DS_Store +``` + +--- + +## 6. CI/CD SECURITY PIPELINE + +### GitHub Actions Workflow: +```yaml +name: Security Scan + +on: [push, pull_request] + +jobs: + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run npm audit + run: npm audit --audit-level=moderate + + - name: Run ESLint security + run: npx eslint . --ext .js + + - name: Run Snyk scan + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + run: npx snyk test + + - name: SonarCloud scan + uses: SonarSource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} +``` + +--- + +## 7. MONITORING & LOGGING + +### Security Event Logging: +```javascript +const logger = require('winston'); + +const securityLogger = logger.createLogger({ + level: 'info', + format: logger.format.json(), + transports: [ + new logger.transports.File({ filename: 'security.log' }), + new logger.transports.Console() + ] +}); + +// Log security events +function logSecurityEvent(event, user, details) { + securityLogger.info({ + timestamp: new Date().toISOString(), + event, + user, + details, + ip: details.ip, + userAgent: details.userAgent + }); +} +``` + +### Audit Trail: +```javascript +const auditSchema = new mongoose.Schema({ + userId: String, + action: String, + resource: String, + changes: Object, + timestamp: { type: Date, default: Date.now }, + ipAddress: String, + userAgent: String +}); +``` + +--- + +## 8. PACKAGE DEPENDENCIES + +### Add to package.json: +```json +{ + "dependencies": { + "bcrypt": "^5.1.1", + "jsonwebtoken": "^9.1.2", + "helmet": "^7.1.0", + "express-rate-limit": "^7.1.5", + "express-mongo-sanitize": "^2.2.0", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "winston": "^3.11.0", + "joi": "^17.11.0" + }, + "devDependencies": { + "eslint": "^8.54.0", + "eslint-plugin-security": "^1.7.1", + "snyk": "^1.1277.0" + } +} +``` + +--- + +## 9. DEPLOYMENT CHECKLIST + +- [ ] Environment variables configured +- [ ] SSL/TLS certificates installed +- [ ] Database backups automated +- [ ] Security headers enabled +- [ ] Rate limiting activated +- [ ] Monitoring enabled +- [ ] Audit logging active +- [ ] Secrets rotated +- [ ] Dependencies updated +- [ ] Security tests passing +- [ ] CORS properly configured +- [ ] HTTPS enforced +- [ ] Security headers validated + +--- + +## 10. DEPLOYMENT STEPS + +1. **Backup Current Code:** + ```bash + git checkout -b security-hardening + ``` + +2. **Install Dependencies:** + ```bash + npm install bcrypt jsonwebtoken helmet express-rate-limit express-mongo-sanitize + npm install --save-dev eslint eslint-plugin-security + ``` + +3. **Update Database Module:** + - Replace SHA256 with bcrypt + - Add input validation + - Add error handling + +4. **Add Middleware:** + - Helmet for security headers + - Rate limiting + - CORS configuration + - MongoDB sanitization + +5. **Configure Environment:** + - Create .env file + - Set all required variables + - Test locally + +6. **Run Security Scans:** + ```bash + npm audit + npx snyk test + npx eslint . --ext .js + ``` + +7. **Deploy:** + ```bash + git push origin security-hardening + # Create pull request for review + ``` + +--- + +## CRITICAL ACTIONS REQUIRED + +1. ✅ **Rotate all credentials** that may have been exposed +2. ✅ **Update password hashing** to bcrypt immediately +3. ✅ **Enable MFA** for all admin accounts +4. ✅ **Review access logs** for suspicious activity +5. ✅ **Implement rate limiting** on all login endpoints +6. ✅ **Add comprehensive audit logging** +7. ✅ **Set up monitoring alerts** + +--- + +**Generated:** 2026-07-13 +**Status:** RECOMMENDED FOR IMMEDIATE IMPLEMENTATION diff --git a/database/database.js b/database/database.js index 4b628fd..4833c19 100644 --- a/database/database.js +++ b/database/database.js @@ -1,4 +1,5 @@ const mongoose = require('mongoose'); +const bcrypt = require('bcrypt'); const config = require('../config'); const crypto = require('crypto'); const user = require('./models/user'); @@ -6,191 +7,496 @@ const bot = require('./models/bot'); const cache = require('../api/cache'); mongoose.set('strictQuery', true); -mongoose.connect(config.dbUri); +mongoose.connect(config.dbUri, { + maxPoolSize: 10, + serverSelectionTimeoutMS: 5000, +}); const User = mongoose.model('User', user); const Bot = mongoose.model('Bot', bot); -async function createUser(username, password) { - const user = new User({ - username: username, - password: crypto.createHash('sha256').update(password).digest('hex'), - encryptedKey: crypto.randomBytes(16).toString('hex'), - tasks: [], - builds: [] - }); +const SALT_ROUNDS = 12; +const MAX_LOGIN_ATTEMPTS = 5; +const LOCKOUT_TIME = 15 * 60 * 1000; // 15 minutes - await user.save(); +async function createUser(username, password) { + try { + // Validate input + if (!username || typeof username !== 'string' || username.length < 3) { + throw new Error('Invalid username - minimum 3 characters'); + } + if (!password || typeof password !== 'string' || password.length < 8) { + throw new Error('Password must be at least 8 characters'); + } - cache.reset(); + // Check if user exists + const existingUser = await User.findOne({ username }); + if (existingUser) { + throw new Error('User already exists'); + } - return user.id; + // Hash password with bcrypt + const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS); + + const newUser = new User({ + username: username, + password: hashedPassword, + encryptedKey: crypto.randomBytes(32).toString('hex'), + tasks: [], + builds: [], + createdAt: new Date(), + lastLogin: null, + loginAttempts: 0, + lockoutUntil: null + }); + + await newUser.save(); + cache.reset(); + + return newUser._id; + } catch (error) { + throw new Error(`Failed to create user: ${error.message}`); + } } async function getUser(id) { - const possible = await User.find({ _id: id }).exec(); - if (possible.length == 0) { + try { + // Sanitize ID + if (!mongoose.Types.ObjectId.isValid(id)) { + return null; + } + + const possible = await User.findById(id).select('-password').exec(); + return possible || null; + } catch (error) { + console.error(`Error fetching user: ${error.message}`); return null; } - - return possible[0]; } async function verifyLogin(username, password) { - const possible = await User.find({ username: username, password: crypto.createHash('sha256').update(password).digest('hex') }).exec(); - if (possible.length == 0) { - return null; - } + try { + // Validate inputs + if (!username || !password) { + return null; + } - return possible[0]; -} + const userDoc = await User.findOne({ username }).exec(); + + if (!userDoc) { + return null; + } -async function changePassword(username, password) { - await User.findOneAndUpdate({ username: username }, { password: crypto.createHash('sha256').update(password).digest('hex') }).exec(); -} + // Check if account is locked + if (userDoc.lockoutUntil && userDoc.lockoutUntil > Date.now()) { + throw new Error('Account temporarily locked due to failed login attempts. Try again later.'); + } -async function addTask(username, goal, param, targeted, countries) { - const possible = await User.find({ username: username }).exec(); - if (possible.length == 0) { - return null; + // Compare passwords using bcrypt + const passwordMatch = await bcrypt.compare(password, userDoc.password); + + if (passwordMatch) { + // Reset login attempts on successful login + userDoc.loginAttempts = 0; + userDoc.lockoutUntil = null; + userDoc.lastLogin = new Date(); + await userDoc.save(); + return userDoc; + } else { + // Increment failed login attempts + userDoc.loginAttempts = (userDoc.loginAttempts || 0) + 1; + + // Lock account after MAX_LOGIN_ATTEMPTS failed attempts + if (userDoc.loginAttempts >= MAX_LOGIN_ATTEMPTS) { + userDoc.lockoutUntil = new Date(Date.now() + LOCKOUT_TIME); + } + + await userDoc.save(); + return null; + } + } catch (error) { + throw new Error(`Login verification failed: ${error.message}`); } +} - let user = possible[0]; +async function changePassword(username, oldPassword, newPassword) { + try { + // Validate inputs + if (!username || !oldPassword || !newPassword) { + throw new Error('Missing required fields'); + } - user.tasks.push({ - goal: goal, - param: param, - date: Date.now(), - targeted: targeted, - completed: 0, - countries: countries - }); + if (newPassword.length < 8) { + throw new Error('New password must be at least 8 characters'); + } - cache.reset(); + if (oldPassword === newPassword) { + throw new Error('New password must be different from current password'); + } - await User.findOneAndUpdate({ username: username }, user); -} + const userDoc = await User.findOne({ username }).exec(); + if (!userDoc) { + throw new Error('User not found'); + } -async function addBuild(username, log, path) { - const possible = await User.find({ username: username }).exec(); - if (possible.length == 0) { - return null; + // Verify old password + const passwordMatch = await bcrypt.compare(oldPassword, userDoc.password); + if (!passwordMatch) { + throw new Error('Current password is incorrect'); + } + + // Hash new password + const hashedPassword = await bcrypt.hash(newPassword, SALT_ROUNDS); + userDoc.password = hashedPassword; + userDoc.passwordChangedAt = new Date(); + await userDoc.save(); + } catch (error) { + throw new Error(`Password change failed: ${error.message}`); } +} - let user = possible[0]; +async function addTask(username, goal, param, targeted, countries) { + try { + // Validate inputs + if (!username || !goal) { + throw new Error('Missing required fields: username and goal'); + } - user.builds.push({ - date: Date.now(), - log: log, - path: path - }); + const userDoc = await User.findOne({ username }).exec(); + if (!userDoc) { + throw new Error('User not found'); + } - await User.findOneAndUpdate({ username: username }, user); + const taskId = new mongoose.Types.ObjectId(); + userDoc.tasks.push({ + _id: taskId, + goal, + param, + date: Date.now(), + targeted, + completed: 0, + countries, + status: 'pending' + }); + + cache.reset(); + await userDoc.save(); + return userDoc; + } catch (error) { + throw new Error(`Failed to add task: ${error.message}`); + } } -async function addUser(username, log, path) { - const possible = await User.find({ username: username }).exec(); - if (possible.length == 0) { - return null; +async function addBuild(username, log, path) { + try { + // Validate inputs + if (!username || !log || !path) { + throw new Error('Missing required fields'); + } + + const userDoc = await User.findOne({ username }).exec(); + if (!userDoc) { + throw new Error('User not found'); + } + + const buildId = new mongoose.Types.ObjectId(); + userDoc.builds.push({ + _id: buildId, + date: Date.now(), + log, + path, + status: 'completed' + }); + + await userDoc.save(); + return userDoc; + } catch (error) { + throw new Error(`Failed to add build: ${error.message}`); } +} - let user = possible[0]; +async function addUser(username, log, path) { + try { + // Validate inputs + if (!username || !log || !path) { + throw new Error('Missing required fields'); + } - user.builds.push({ - date: Date.now(), - log: log, - path: path - }); + const userDoc = await User.findOne({ username }).exec(); + if (!userDoc) { + throw new Error('User not found'); + } - await User.findOneAndUpdate({ username: username }, user); + const buildId = new mongoose.Types.ObjectId(); + userDoc.builds.push({ + _id: buildId, + date: Date.now(), + log, + path, + status: 'completed' + }); + + await userDoc.save(); + return userDoc; + } catch (error) { + throw new Error(`Failed to add user build: ${error.message}`); + } } async function userExist(username) { - const possible = await User.find({ username: username }).exec(); - if (possible.length == 0) { + try { + if (!username) { + return false; + } + + const user = await User.findOne({ username }).exec(); + return !!user; + } catch (error) { + console.error(`Error checking user existence: ${error.message}`); return false; } - - return true; } async function addBot(ownerusername, hostname, ip, country, av, os) { - const bot = new Bot({ - ownerusername: ownerusername, - hostname: hostname, - ip: ip, - country: country, - av: av, - os: os, - firstStart: Date.now(), - lastPing: Date.now() - }); + try { + // Validate inputs + if (!ownerusername || !hostname || !ip) { + throw new Error('Missing required fields'); + } + + // Validate IP format + if (!isValidIP(ip)) { + throw new Error('Invalid IP address format'); + } - await bot.save(); + // Check user exists + const userExists = await User.findOne({ username: ownerusername }).exec(); + if (!userExists) { + throw new Error('User not found'); + } - return bot.id; + const newBot = new Bot({ + ownerusername, + hostname, + ip, + country, + av, + os, + firstStart: Date.now(), + lastPing: Date.now(), + status: 'active' + }); + + await newBot.save(); + return newBot._id; + } catch (error) { + throw new Error(`Failed to add bot: ${error.message}`); + } } async function newPing(id) { - await Bot.findOneAndUpdate({ _id: id }, { lastPing: Date.now() }); + try { + if (!mongoose.Types.ObjectId.isValid(id)) { + throw new Error('Invalid bot ID'); + } + + const botDoc = await Bot.findByIdAndUpdate( + id, + { lastPing: Date.now() }, + { new: true } + ); + + if (!botDoc) { + throw new Error('Bot not found'); + } + + return botDoc; + } catch (error) { + throw new Error(`Failed to update ping: ${error.message}`); + } } async function getTasks(username) { - const possible = await User.find({ username: username }).lean().exec(); - if (possible.length == 0) { + try { + if (!username) { + return null; + } + + const possibleUsers = await User.findOne({ username }).lean().exec(); + return possibleUsers?.tasks || null; + } catch (error) { + console.error(`Error fetching tasks: ${error.message}`); return null; } - - return possible[0].tasks; } async function getBuilds(username) { - const possible = await User.find({ username: username }).lean().exec(); - if (possible.length == 0) { + try { + if (!username) { + return null; + } + + const possibleUsers = await User.findOne({ username }).lean().exec(); + return possibleUsers?.builds || null; + } catch (error) { + console.error(`Error fetching builds: ${error.message}`); return null; } - - return possible[0].builds; } async function getBots(username) { - const possible = await Bot.find({ ownerusername: username }).lean().exec(); - return possible; + try { + if (!username) { + return []; + } + + const bots = await Bot.find({ ownerusername: username }).lean().exec(); + return bots || []; + } catch (error) { + console.error(`Error fetching bots: ${error.message}`); + return []; + } } async function deleteBot(id) { - await Bot.deleteOne({ _id: id }); + try { + if (!mongoose.Types.ObjectId.isValid(id)) { + throw new Error('Invalid bot ID'); + } + + const result = await Bot.deleteOne({ _id: id }); + + if (result.deletedCount === 0) { + throw new Error('Bot not found'); + } + + return true; + } catch (error) { + throw new Error(`Failed to delete bot: ${error.message}`); + } } async function deleteTask(id, username) { - await User.findOneAndUpdate({ username: username }, { $pull: { tasks: { _id: id } } }, { new: true }); + try { + if (!mongoose.Types.ObjectId.isValid(id)) { + throw new Error('Invalid task ID'); + } + + if (!username) { + throw new Error('Username required'); + } + + const result = await User.findOneAndUpdate( + { username }, + { $pull: { tasks: { _id: id } } }, + { new: true } + ); + + if (!result) { + throw new Error('User not found'); + } + + return result; + } catch (error) { + throw new Error(`Failed to delete task: ${error.message}`); + } } async function deleteBuild(id, username) { - await User.findOneAndUpdate({ username: username }, { $pull: { builds: { _id: id } } }, { new: true }); + try { + if (!mongoose.Types.ObjectId.isValid(id)) { + throw new Error('Invalid build ID'); + } + + if (!username) { + throw new Error('Username required'); + } + + const result = await User.findOneAndUpdate( + { username }, + { $pull: { builds: { _id: id } } }, + { new: true } + ); + + if (!result) { + throw new Error('User not found'); + } + + return result; + } catch (error) { + throw new Error(`Failed to delete build: ${error.message}`); + } } async function addCompleted(id, taskid) { - await User.updateOne({ _id: id, 'tasks._id': taskid }, { $inc: { 'tasks.$.completed': 1}}) + try { + if (!mongoose.Types.ObjectId.isValid(id) || !mongoose.Types.ObjectId.isValid(taskid)) { + throw new Error('Invalid ID format'); + } + + const result = await User.updateOne( + { _id: id, 'tasks._id': taskid }, + { $inc: { 'tasks.$.completed': 1 } } + ); + + if (result.matchedCount === 0) { + throw new Error('User or task not found'); + } + + return result; + } catch (error) { + throw new Error(`Failed to update task completion: ${error.message}`); + } } async function getBuild(username, buildid) { - const user = await User.find({ username: username}).exec(); - const build = user[0].builds.id(buildid); + try { + if (!username || !buildid) { + throw new Error('Missing required parameters'); + } - return build; -} + if (!mongoose.Types.ObjectId.isValid(buildid)) { + throw new Error('Invalid build ID'); + } -async function clearOldBots() { - const botsDb = await Bot.find({}).exec(); + const userDoc = await User.findOne({ username }).exec(); + if (!userDoc) { + throw new Error('User not found'); + } - for (let i = 0; i < botsDb.length; i++) { - if ((Date.now() - botsDb[i].lastPing) > 1000*60*60*24*5) { - await deleteBot(botsDb[i]._id); + const build = userDoc.builds.id(buildid); + if (!build) { + throw new Error('Build not found'); } + + return build; + } catch (error) { + throw new Error(`Failed to retrieve build: ${error.message}`); } } +async function clearOldBots() { + try { + const FIVE_DAYS_MS = 1000 * 60 * 60 * 24 * 5; + const cutoffTime = Date.now() - FIVE_DAYS_MS; + + const result = await Bot.deleteMany({ lastPing: { $lt: cutoffTime } }); + console.log(`[DB] Cleared ${result.deletedCount} inactive bots`); + return result.deletedCount; + } catch (error) { + console.error(`Error clearing old bots: ${error.message}`); + return 0; + } +} + +function isValidIP(ip) { + const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/; + if (!ipv4Regex.test(ip)) return false; + + const parts = ip.split('.').map(Number); + return parts.every(part => part >= 0 && part <= 255); +} + module.exports = { createUser, verifyLogin, @@ -202,7 +508,6 @@ module.exports = { getBots, getBuild, userExist, - getBots, addBuild, changePassword, deleteTask, @@ -212,4 +517,4 @@ module.exports = { newPing, clearOldBots, addCompleted -} \ No newline at end of file +}; \ No newline at end of file diff --git a/middleware/security.js b/middleware/security.js new file mode 100644 index 0000000..29c6c8b --- /dev/null +++ b/middleware/security.js @@ -0,0 +1,274 @@ +const helmet = require('helmet'); +const rateLimit = require('express-rate-limit'); +const mongoSanitize = require('express-mongo-sanitize'); +const cors = require('cors'); +const jwt = require('jsonwebtoken'); +const logger = require('winston'); + +// Configure logger +const securityLogger = logger.createLogger({ + level: 'info', + format: logger.format.json(), + transports: [ + new logger.transports.File({ filename: 'logs/security.log' }), + new logger.transports.Console() + ] +}); + +/** + * Security Headers Middleware + * Protects against common attacks using Helmet + */ +const securityHeaders = helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'"], + scriptSrc: ["'self'"], + imgSrc: ["'self'", 'data:', 'https:'], + }, + }, + frameguard: { action: 'deny' }, + noSniff: true, + xssFilter: true, + referrerPolicy: { policy: 'strict-origin-when-cross-origin' }, +}); + +/** + * Rate Limiting Middleware + * Prevents brute force attacks + */ +const globalLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: 'Too many requests from this IP, please try again later.', + standardHeaders: true, + legacyHeaders: false, + skip: (req) => { + // Skip rate limiting for health checks + return req.path === '/health'; + } +}); + +/** + * Stricter Rate Limiting for Login + */ +const loginLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 5, // Limit to 5 login attempts per 15 minutes + message: 'Too many login attempts, please try again later.', + standardHeaders: true, + legacyHeaders: false, + keyGenerator: (req) => { + // Rate limit by username + IP for better security + return `${req.body.username}:${req.ip}`; + } +}); + +/** + * Rate Limiting for API endpoints + */ +const apiLimiter = rateLimit({ + windowMs: 60 * 1000, // 1 minute + max: 30, // 30 requests per minute + message: 'Too many API requests, please try again later.', + standardHeaders: true, + legacyHeaders: false +}); + +/** + * CORS Configuration + */ +const corsOptions = { + origin: (origin, callback) => { + const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000']; + + if (!origin || allowedOrigins.includes(origin)) { + callback(null, true); + } else { + callback(new Error('Not allowed by CORS')); + } + }, + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization'], + maxAge: 86400 // 24 hours +}; + +const corsMiddleware = cors(corsOptions); + +/** + * MongoDB Injection Prevention + */ +const mongoSanitizeMiddleware = mongoSanitize({ + replaceWith: '_', + onSanitize: ({ req, key }) => { + securityLogger.warn({ + event: 'MongoDB_Injection_Attempt', + key, + ip: req.ip, + timestamp: new Date().toISOString() + }); + }, +}); + +/** + * Request body size limiting + */ +const bodyLimiter = (req, res, next) => { + const maxBodySize = 1024 * 1024; // 1MB + + if (req.headers['content-length'] > maxBodySize) { + return res.status(413).json({ error: 'Payload too large' }); + } + + next(); +}; + +/** + * JWT Authentication Middleware + */ +const authenticateToken = (req, res, next) => { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) { + return res.status(401).json({ error: 'Access token required' }); + } + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + req.user = decoded; + next(); + } catch (error) { + securityLogger.warn({ + event: 'Invalid_JWT_Token', + error: error.message, + ip: req.ip, + timestamp: new Date().toISOString() + }); + + return res.status(403).json({ error: 'Invalid or expired token' }); + } +}; + +/** + * Input Validation Middleware + */ +const validateInput = (schema) => { + return (req, res, next) => { + const { error, value } = schema.validate(req.body); + + if (error) { + securityLogger.warn({ + event: 'Invalid_Input', + error: error.details[0].message, + ip: req.ip, + timestamp: new Date().toISOString() + }); + + return res.status(400).json({ + error: 'Invalid input', + details: error.details[0].message + }); + } + + req.body = value; + next(); + }; +}; + +/** + * Security Event Logging Middleware + */ +const securityEventLogger = (req, res, next) => { + // Log security-relevant events + if (req.path.includes('/login') || req.path.includes('/auth')) { + securityLogger.info({ + event: 'Auth_Attempt', + method: req.method, + path: req.path, + ip: req.ip, + userAgent: req.headers['user-agent'], + timestamp: new Date().toISOString() + }); + } + + next(); +}; + +/** + * Error Handling Middleware + */ +const errorHandler = (err, req, res, next) => { + securityLogger.error({ + event: 'Error', + error: err.message, + stack: err.stack, + ip: req.ip, + path: req.path, + timestamp: new Date().toISOString() + }); + + // Don't leak error details in production + const statusCode = err.statusCode || 500; + const message = process.env.NODE_ENV === 'production' + ? 'Internal server error' + : err.message; + + res.status(statusCode).json({ error: message }); +}; + +/** + * HTTPS Enforcement Middleware + */ +const enforceHTTPS = (req, res, next) => { + if (process.env.NODE_ENV === 'production' && req.header('x-forwarded-proto') !== 'https') { + return res.redirect(301, `https://${req.header('host')}${req.url}`); + } + + next(); +}; + +/** + * Security Headers - Additional + */ +const securityHeadersAdditional = (req, res, next) => { + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('X-Frame-Options', 'DENY'); + res.setHeader('X-XSS-Protection', '1; mode=block'); + res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); + res.setHeader('Content-Security-Policy', "default-src 'self'"); + res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); + res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()'); + + next(); +}; + +/** + * Request ID Middleware for tracking + */ +const requestIdMiddleware = (req, res, next) => { + const crypto = require('crypto'); + req.id = crypto.randomUUID(); + res.setHeader('X-Request-ID', req.id); + next(); +}; + +module.exports = { + securityHeaders, + globalLimiter, + loginLimiter, + apiLimiter, + corsMiddleware, + mongoSanitizeMiddleware, + bodyLimiter, + authenticateToken, + validateInput, + securityEventLogger, + errorHandler, + enforceHTTPS, + securityHeadersAdditional, + requestIdMiddleware, + securityLogger +}; \ No newline at end of file