From 89056f15273fceb103d9676db40fc2cdee8b7047 Mon Sep 17 00:00:00 2001 From: pavsoss Date: Mon, 13 Jul 2026 03:42:13 +0530 Subject: [PATCH 1/2] chore: remove dead and unused modules --- backend/app.py | 253 ----------- backend/middleware/fileValidation.js | 236 ---------- backend/middleware/filevalidation.js | 462 -------------------- backend/middleware/rateLimiter.js.bak | 361 --------------- backend/middleware/validation.middleware.js | 34 -- backend/routes/Tuple[str | 0 backend/routes/bool | 0 backend/routes/dict | 0 backend/routes/prediction.route.js | 41 -- backend/tests/fileValidation.test.js | 279 ------------ backend/utils/fileValidation.js | 269 ------------ backend/utils/utils/banner.js | 16 - backend/validators/auth.validator.js | 28 -- backend/validators/prediction.validator.js | 183 -------- domain_checker.py | 195 --------- frontend/frontend/src/components/Navbar.jsx | 49 --- label_encoder.pkl | Bin 497 -> 0 bytes linear_svm_model.pkl | Bin 120763 -> 0 bytes tatus | 5 - tfidf_vectorizer.pkl | 0 20 files changed, 2411 deletions(-) delete mode 100644 backend/app.py delete mode 100644 backend/middleware/fileValidation.js delete mode 100644 backend/middleware/filevalidation.js delete mode 100644 backend/middleware/rateLimiter.js.bak delete mode 100644 backend/middleware/validation.middleware.js delete mode 100644 backend/routes/Tuple[str delete mode 100644 backend/routes/bool delete mode 100644 backend/routes/dict delete mode 100644 backend/routes/prediction.route.js delete mode 100644 backend/tests/fileValidation.test.js delete mode 100644 backend/utils/fileValidation.js delete mode 100644 backend/utils/utils/banner.js delete mode 100644 backend/validators/auth.validator.js delete mode 100644 backend/validators/prediction.validator.js delete mode 100644 domain_checker.py delete mode 100644 frontend/frontend/src/components/Navbar.jsx delete mode 100644 label_encoder.pkl delete mode 100644 linear_svm_model.pkl delete mode 100644 tatus delete mode 100644 tfidf_vectorizer.pkl diff --git a/backend/app.py b/backend/app.py deleted file mode 100644 index d45e460c..00000000 --- a/backend/app.py +++ /dev/null @@ -1,253 +0,0 @@ -import time -import logging -from logging.handlers import RotatingFileHandler -from flask import request, g -from flask import Flask,request,jsonify -import os -import csv -import joblib -import re -from collections import Counter -from datetime import datetime -from dotenv import load_dotenv -from flask_limiter import Limiter -from flask_limiter.util import get_remote_address -import numpy as np -from utils.spamSeverity import calculate_spam_severity - -load_dotenv() - -app=Flask(__name__) - -# ─── LOGGING CONFIGURATION ────────────────────────────────────── -log_handler = RotatingFileHandler('api.log', maxBytes=10485760, backupCount=5) -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s', - handlers=[ - log_handler, - logging.StreamHandler() - ] -) -logger = logging.getLogger(__name__) -app.logger.addHandler(log_handler) - -# ─── REQUEST LOGGING MIDDLEWARE ──────────────────────────────── -@app.before_request -def start_timer(): - g.start = time.time() - g.client_ip = request.remote_addr - -@app.after_request -def log_request(response): - duration = time.time() - g.start - logger.info(f"{request.method} {request.path} → {response.status_code} ({duration:.3f}s) from {g.client_ip}") - return response - -# ─── RATE LIMITER ──────────────────────────────────────────────── -def get_forwarded_address(): - return request.headers.get("X-Forwarded-For", request.remote_addr) - -limiter = Limiter( - app=app, - key_func=get_forwarded_address, - default_limits=["10 per minute"], - storage_uri="memory://", - strategy="fixed-window", -) - -# ─── RATE LIMIT ERROR HANDLER ────────────────────────────────── -@app.errorhandler(429) -def ratelimit_handler(e): - return jsonify({ - "error": "rate_limit_exceeded", - "message": "Too many requests. Limit is 10 per minute. Please try again in 60 seconds.", - "retry_after": 60 - }), 429 - -FEEDBACK_FILE = 'feedback_store.csv' - -def ensure_feedback_file(): - if not os.path.exists(FEEDBACK_FILE): - with open(FEEDBACK_FILE, 'w', newline='', encoding='utf-8') as f: - writer = csv.writer(f) - writer.writerow(['text', 'predicted_label', 'correct_label', 'submitted_at']) - -# ─── LOAD MODELS ──────────────────────────────────────────────── -MODEL_PATH=os.getenv("MODEL_PATH") -VECTORIZER_PATH=os.getenv("VECTORIZER_PATH") -LABEL_ENCODER_PATH = os.getenv("LABEL_ENCODER_PATH") - -if not MODEL_PATH or not VECTORIZER_PATH or not LABEL_ENCODER_PATH: - raise ValueError("Required environment variables are missing") - -model = joblib.load(MODEL_PATH) -vectorizer = joblib.load(VECTORIZER_PATH) -label_encoder = joblib.load(LABEL_ENCODER_PATH) - - -@app.route("/") -def home(): - return "ML API Running 🚀" - -@app.route('/health', methods=['GET']) -def health_check(): - return jsonify({ - 'status': 'healthy', - 'model_loaded': model is not None, - 'vectorizer_loaded': vectorizer is not None - }) - - -# ─── HEALTH CHECK ENDPOINT ─────────────────────────────────────── -@app.route("/health", methods=["GET"]) -def health_check(): - """Check if the API and all components are healthy.""" - status = { - "status": "healthy", - "model_loaded": model is not None, - "vectorizer_loaded": vectorizer is not None, - "label_encoder_loaded": label_encoder is not None, - "timestamp": datetime.now().isoformat() - } - - # Check if all components are loaded - if not status["model_loaded"] or not status["vectorizer_loaded"] or not status["label_encoder_loaded"]: - status["status"] = "degraded" - status["message"] = "One or more components failed to load" - return jsonify(status), 503 - - # Check if model can predict (optional smoke test) - try: - test_result = model.predict(vectorizer.transform(["test"])) - status["smoke_test"] = "passed" - except Exception as e: - status["smoke_test"] = "failed" - status["smoke_test_error"] = str(e) - status["status"] = "degraded" - return jsonify(status), 503 - - return jsonify(status), 200 - - -def make_prediction_response( - input_text, - result, - confidence_score, - decision_score, - confidence_level, - detected_language="en", - translated=False, - translated_text=None, - domain_analysis=None, - explanation=None, - severity=None -): - """Enforces a strict standardized response schema for all predictions.""" - response = { - "input": input_text, - "result": result, - "prediction": result, - "confidence": round(float(confidence_score) / 100.0, 4) if confidence_score is not None else 0.0, - "confidence_score": float(confidence_score) if confidence_score is not None else 0.0, - "decision_score": float(decision_score) if decision_score is not None else None, - "confidence_level": confidence_level, - "detected_language": detected_language, - "translated": translated - } - if translated and translated_text: - response["translated_text"] = translated_text - if domain_analysis is not None: - response["domain_analysis"] = domain_analysis - if explanation is not None: - response["explanation"] = explanation - if severity is not None: - response["severity"] = severity - return response - - -# ─── PREDICT ENDPOINT ──────────────────────────────────────────── -@app.route("/predict", methods=["POST"]) -@limiter.limit("10 per minute") -def predict(): - try: - data = request.get_json() - text = data.get("text") - - if not text: - logger.warning("No text provided for prediction") - return jsonify({"error": "No text provided"}), 400 - - # Translate incoming text to English if it is not in English - original_text = text - detected_language = "en" - translated = False - - if text.strip(): - try: - from langdetect import detect - detected_language = detect(text) - except Exception: - detected_language = "en" - - if detected_language != "en": - try: - from deep_translator import GoogleTranslator - translated_text = GoogleTranslator(source='auto', target='en').translate(text) - if translated_text and translated_text.strip().lower() != text.strip().lower(): - text = translated_text - translated = True - except Exception: - pass - - text_vector = vectorizer.transform([text]) - prediction = model.predict(text_vector) - final_output = label_encoder.inverse_transform(prediction)[0] - - logger.info(f"Prediction: '{text[:50]}...' -> {final_output}") - - import numpy as np - decision_score = None - confidence_score = 95.0 - try: - if hasattr(model, "decision_function"): - decision = model.decision_function(text_vector) - if isinstance(decision, np.ndarray): - decision_score = float(np.max(np.abs(decision))) - else: - decision_score = float(abs(decision)) - # Convert to pseudo‑probability - prob = 1.0 / (1.0 + np.exp(-decision_score)) - confidence_score = round(prob * 100, 2) - except Exception: - confidence_score = 0.0 - decision_score = None - - if confidence_score >= 80: - confidence_level = "high" - elif confidence_score >= 60: - confidence_level = "medium" - else: - confidence_level = "low" - - response_data = make_prediction_response( - input_text=original_text, - result=final_output, - confidence_score=confidence_score, - decision_score=decision_score, - confidence_level=confidence_level, - detected_language=detected_language, - translated=translated, - translated_text=text if translated else None, - severity=calculate_spam_severity(original_text) - ) - return jsonify(response_data) - - except Exception as e: - logger.error(f"Prediction error: {str(e)}") - return jsonify({"error": str(e)}), 500 - -if __name__ == "__main__": - FLASK_PORT = int(os.getenv("FLASK_PORT", 5000)) - app.run(host="0.0.0.0", port=FLASK_PORT, debug=True) - diff --git a/backend/middleware/fileValidation.js b/backend/middleware/fileValidation.js deleted file mode 100644 index bb126ad8..00000000 --- a/backend/middleware/fileValidation.js +++ /dev/null @@ -1,236 +0,0 @@ -const multer = require('multer'); -const net = require('net'); - -/** - * Parses a single CSV line handling quoted fields and escaped quotes according to RFC 4180. - * @param {string} line - * @returns {string[]} - */ -function parseCSVLine(line) { - const result = []; - let current = ''; - let inQuotes = false; - for (let i = 0; i < line.length; i++) { - const char = line[i]; - if (char === '"') { - if (inQuotes) { - const nextChar = line[i + 1]; - const charAfterNext = line[i + 2]; - if (nextChar === '"' && charAfterNext !== ',' && charAfterNext !== undefined && charAfterNext !== '\r' && charAfterNext !== '\n') { - // It is an escaped quote - current += '"'; - i++; - } else if (nextChar === ',' || nextChar === undefined || nextChar === '\r' || nextChar === '\n') { - // Closes the field - inQuotes = false; - } else { - // Literal quote - current += '"'; - } - } else { - if (current === '') { - inQuotes = true; - } else { - current += '"'; - } - } - } else if (char === ',' && !inQuotes) { - result.push(current); - current = ''; - } else { - current += char; - } - } - result.push(current); - return result; -} - -/** - * Sanitizes a CSV cell value to prevent XSS and CSV Formula Injection attacks. - * @param {any} value - * @returns {any} - */ -function sanitizeCSVCell(value) { - if (typeof value !== 'string') return value; - - // Escape basic XSS vectors (specifically < and >) - let sanitized = value.replace(//g, '>'); - - // Neutralize formula injection: if starts with =, +, -, @, prefix with ' - if (/^[=\+\-@]/.test(sanitized)) { - sanitized = "'" + sanitized; - } - - return sanitized; -} - -/** - * Scan buffer for malware via ClamAV (optional TCP interface) - * @param {Buffer} buffer - * @returns {Promise} Resolves to true if clean, false if infected/malware - */ -async function scanWithClamAV(buffer) { - return new Promise((resolve, reject) => { - const host = process.env.CLAMAV_HOST || 'localhost'; - const port = parseInt(process.env.CLAMAV_PORT || '3310', 10); - - const socket = new net.Socket(); - socket.setTimeout(5000); - - socket.connect(port, host, () => { - // Send zINSTREAM command (modern ClamAV null-terminated command prefix) - const prefix = Buffer.from('zINSTREAM\0'); - socket.write(prefix); - - // Send chunk size (4 bytes, big endian) followed by chunk data - const sizeBuf = Buffer.alloc(4); - sizeBuf.writeUInt32BE(buffer.length, 0); - socket.write(sizeBuf); - socket.write(buffer); - - // Send zero-length chunk to indicate end of stream - const endBuf = Buffer.alloc(4); - endBuf.writeUInt32BE(0, 0); - socket.write(endBuf); - }); - - let response = ''; - socket.on('data', (data) => { - response += data.toString(); - }); - - socket.on('end', () => { - if (response.includes('FOUND')) { - resolve(false); // Malware found - } else { - resolve(true); // Clean - } - }); - - socket.on('error', (err) => { - reject(err); - }); - - socket.on('timeout', () => { - socket.destroy(); - reject(new Error('ClamAV scan timeout')); - }); - }); -} - -// Multer in-memory storage config with size limits -const storage = multer.memoryStorage(); -const upload = multer({ - storage: storage, - limits: { - fileSize: 20 * 1024 * 1024 // Set slightly higher to let middleware handle custom size limit logic and error codes - }, - fileFilter: (req, file, cb) => { - const fileExtension = file.originalname.split('.').pop().toLowerCase(); - if (fileExtension !== 'csv') { - return cb(new Error('Only CSV files are allowed'), false); - } - cb(null, true); - } -}).single('file'); - -/** - * Express middleware to validate and sanitize uploaded CSV files. - */ -const validateCSVUpload = (req, res, next) => { - upload(req, res, async (err) => { - if (err) { - if (err.code === 'LIMIT_FILE_SIZE') { - return res.status(413).json({ error: 'File too large' }); - } - return res.status(400).json({ error: err.message }); - } - - if (!req.file) { - return res.status(400).json({ error: 'No file uploaded' }); - } - - // Custom file size limit check from env - const maxFileSize = parseInt(process.env.MAX_CSV_FILE_SIZE, 10) || 5 * 1024 * 1024; - if (req.file.size > maxFileSize) { - return res.status(413).json({ error: 'File too large' }); - } - - const buffer = req.file.buffer; - - // Optional ClamAV Malware Scan - if (process.env.ENABLE_MALWARE_SCAN === 'true') { - try { - const isClean = await scanWithClamAV(buffer); - if (!isClean) { - return res.status(400).json({ error: 'File contains malware' }); - } - } catch (scanError) { - console.error('Malware scan failed:', scanError); - return res.status(500).json({ error: 'Malware scan service unavailable' }); - } - } - - const fileContent = buffer.toString('utf-8'); - if (!fileContent.trim()) { - return res.status(400).json({ error: 'CSV file is empty' }); - } - - const lines = fileContent.split(/\r?\n/).filter(line => line.trim()); - if (lines.length === 0) { - return res.status(400).json({ error: 'CSV file is empty' }); - } - - // Custom row count limit check from env - const maxRows = parseInt(process.env.MAX_CSV_ROWS, 10) || 100000; - if (lines.length - 1 > maxRows) { - return res.status(413).json({ error: 'File too large (too many rows)' }); - } - - // Parse headers and normalize/trim - const rawHeaders = parseCSVLine(lines[0]); - const normalizedHeaders = rawHeaders.map(h => h.trim().toLowerCase()); - - // Must contain "text" or "message" column - const hasText = normalizedHeaders.includes('text') || normalizedHeaders.includes('message'); - if (!hasText) { - return res.status(400).json({ - error: 'Invalid CSV format', - errors: ['CSV file must contain a "text" or "message" column'] - }); - } - - const rows = []; - for (let i = 1; i < lines.length; i++) { - const parsedLine = parseCSVLine(lines[i]); - const rowObj = {}; - rawHeaders.forEach((header, index) => { - const val = parsedLine[index] !== undefined ? parsedLine[index] : ''; - // Store sanitized values in the row object keyed by the parsed header - rowObj[header.trim()] = sanitizeCSVCell(val); - }); - rows.push(rowObj); - - // Yield to the event loop every 500 rows to prevent blocking (DoS) - if (i % 500 === 0) { - await new Promise(resolve => setImmediate(resolve)); - } - } - - req.parsedCSV = { - headers: rawHeaders.map(h => h.trim()), - rows: rows, - totalRows: rows.length, - filename: req.file.originalname, - size: req.file.size - }; - - next(); - }); -}; - -module.exports = { - parseCSVLine, - sanitizeCSVCell, - validateCSVUpload -}; diff --git a/backend/middleware/filevalidation.js b/backend/middleware/filevalidation.js deleted file mode 100644 index 4c57b40b..00000000 --- a/backend/middleware/filevalidation.js +++ /dev/null @@ -1,462 +0,0 @@ -const multer = require('multer'); -const net = require('net'); - - -function parseCSVLine(line) { - const result = []; - let current = ''; - let inQuotes = false; - for (let i = 0; i < line.length; i++) { - const char = line[i]; - if (char === '"') { - if (inQuotes) { - const nextChar = line[i + 1]; - const charAfterNext = line[i + 2]; - if (nextChar === '"' && charAfterNext !== ',' && charAfterNext !== undefined && charAfterNext !== '\r' && charAfterNext !== '\n') { - // It is an escaped quote - current += '"'; - i++; - } else if (nextChar === ',' || nextChar === undefined || nextChar === '\r' || nextChar === '\n') { - // Closes the field - inQuotes = false; - } else { - // Literal quote - current += '"'; - } - } else { - if (current === '') { - inQuotes = true; - } else { - current += '"'; - } - } - } else if (char === ',' && !inQuotes) { - result.push(current); - current = ''; - } else { - current += char; - } - } - result.push(current); - return result; -} - -/** - * Sanitizes a CSV cell value to prevent XSS and CSV Formula Injection attacks. - * @param {any} value - * @returns {any} - */ -function sanitizeCSVCell(value) { - if (typeof value !== 'string') return value; - - // Escape basic XSS vectors (specifically < and >) - let sanitized = value.replace(//g, '>'); - - // Neutralize formula injection: if starts with =, +, -, @, prefix with ' - if (/^[=\+\-@]/.test(sanitized)) { - sanitized = "'" + sanitized; -function sanitizeCSVCell(cell) { - if (!cell || typeof cell !== 'string') { - return cell; - } - - const trimmed = cell.trim(); - - // Dangerous patterns that could indicate formula injection - const dangerousPatterns = [ - /^=.*/i, // Excel formulas - /^\+.*/i, // Excel formulas - /^@.*/i, // Excel formulas - /^-\s*.*/i, // Excel formulas - /^=cmd\|.*/i, // Command execution - /^=hyperlink\(.*\)/i, // Hyperlink injection - /^=dde\(.*\)/i, // DDE execution - /^=system\(.*\)/i, // System command - /^=shell\(.*\)/i, // Shell command - /^=execute\(.*\)/i, // Execute command - /^=run\(.*\)/i, // Run command - /\b(calc|cmd|powershell|bash|sh)\b/i // System commands - ]; - - // Check if cell starts with dangerous pattern - for (const pattern of dangerousPatterns) { - if (pattern.test(trimmed)) { - // Prefix with single quote to neutralize formula - return `'${trimmed}`; - } - } - - // Check for potential XSS in CSV (if rendered as HTML) - const xssPatterns = [ - /.*?<\/script>/i, - //i, - //i, - //i, - //i, - //i, - /on\w+\s*=/i, - /javascript:/i, - /vbscript:/i, - /data:/i - ]; - - for (const pattern of xssPatterns) { - if (pattern.test(cell)) { - // Escape HTML entities - return cell - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - } - } - - return sanitized; -} - -/** - * Scan buffer for malware via ClamAV (optional TCP interface) - * @param {Buffer} buffer - * @returns {Promise} Resolves to true if clean, false if infected/malware - */ -async function scanWithClamAV(buffer) { - return new Promise((resolve, reject) => { - const host = process.env.CLAMAV_HOST || 'localhost'; - const port = parseInt(process.env.CLAMAV_PORT || '3310', 10); - - const socket = new net.Socket(); - socket.setTimeout(5000); - - socket.connect(port, host, () => { - // Send zINSTREAM command (modern ClamAV null-terminated command prefix) - const prefix = Buffer.from('zINSTREAM\0'); - socket.write(prefix); - - // Send chunk size (4 bytes, big endian) followed by chunk data - const sizeBuf = Buffer.alloc(4); - sizeBuf.writeUInt32BE(buffer.length, 0); - socket.write(sizeBuf); - socket.write(buffer); - - // Send zero-length chunk to indicate end of stream - const endBuf = Buffer.alloc(4); - endBuf.writeUInt32BE(0, 0); - socket.write(endBuf); - }); - - let response = ''; - socket.on('data', (data) => { - response += data.toString(); - }); - - socket.on('end', () => { - if (response.includes('FOUND')) { - resolve(false); // Malware found - } else { - resolve(true); // Clean -function validateCSVContent(rows, headers) { - const errors = []; - - // Check if CSV is empty - if (!rows || rows.length === 0) { - errors.push('CSV file is empty'); - } - - // Check row count limit - if (rows.length > MAX_ROWS) { - errors.push(`CSV file exceeds maximum row limit of ${MAX_ROWS}`); - } - - // Check if required columns exist - const requiredColumns = ['text', 'message']; - const hasRequiredColumn = requiredColumns.some(col => - headers.some(h => h.toLowerCase().trim() === col.toLowerCase().trim()) - ); - - if (!hasRequiredColumn) { - errors.push(`CSV must contain a 'text' or 'message' column. Found: ${headers.join(', ')}`); - } - - // Check for empty rows - const emptyRows = rows.filter(row => { - const values = Object.values(row); - return values.every(val => !val || val.trim() === ''); - }); - - if (emptyRows.length > 0) { - errors.push(`Found ${emptyRows.length} empty rows in CSV`); - } - - // Check for very large cell values - const maxCellLength = 10000; // 10k characters - rows.forEach((row, index) => { - Object.values(row).forEach(value => { - if (value && value.length > maxCellLength) { - errors.push(`Row ${index + 1} contains a cell exceeding ${maxCellLength} characters`); - } - }); - - socket.on('error', (err) => { - reject(err); - }); - - socket.on('timeout', () => { - socket.destroy(); - reject(new Error('ClamAV scan timeout')); - }); - }); -} - -// Multer in-memory storage config with size limits -const storage = multer.memoryStorage(); -const upload = multer({ - storage: storage, - limits: { - fileSize: 20 * 1024 * 1024 // Set slightly higher to let middleware handle custom size limit logic and error codes - }, - fileFilter: (req, file, cb) => { - const fileExtension = file.originalname.split('.').pop().toLowerCase(); - if (fileExtension !== 'csv') { - return cb(new Error('Only CSV files are allowed'), false); - } - cb(null, true); - } -}).single('file'); - -/** - * Express middleware to validate and sanitize uploaded CSV files. - */ -const validateCSVUpload = (req, res, next) => { - upload(req, res, async (err) => { - if (err) { - if (err.code === 'LIMIT_FILE_SIZE') { - return res.status(413).json({ error: 'File too large' }); - } - return res.status(400).json({ error: err.message }); -async function scanForMalware(fileBuffer, filename) { - try { - // Check if ClamAV is available - const clamd = require('clamdjs'); - const scanner = clamd.createScanner('localhost', 3310); - - const result = await scanner.scanBuffer(fileBuffer); - - if (result.isInfected) { - throw new Error(`Malware detected: ${result.virusName}`); - } - - return { clean: true }; - } catch (error) { - // If ClamAV is not available, log warning but don't block - if (error.code === 'ECONNREFUSED') { - console.warn('⚠️ ClamAV not available - skipping malware scan'); - return { clean: true, warning: 'ClamAV not available' }; - } - - if (!req.file) { - return res.status(400).json({ error: 'No file uploaded' }); - } - - // Custom file size limit check from env - const maxFileSize = parseInt(process.env.MAX_CSV_FILE_SIZE, 10) || 5 * 1024 * 1024; - if (req.file.size > maxFileSize) { - return res.status(413).json({ error: 'File too large' }); - } -const validateCSVUpload = async (req, res, next) => { - try { - // Use multer to handle file upload - upload.single('file')(req, res, async (err) => { - if (err) { - if (err instanceof multer.MulterError) { - if (err.code === "LIMIT_FILE_SIZE") { - return res.status(413).json({ - success: false, - error: `File too large. Maximum size is ${MAX_FILE_SIZE / (1024 * 1024)}MB` - }); - } - if (err.code === 'LIMIT_FILE_COUNT') { - return res.status(400).json({ - success: false, - error: 'Only one file can be uploaded at a time' - }); - } - return res.status(400).json({ - success: false, - error: `Upload error: ${err.message}` - }); - } - return res.status(400).json({ - success: false, - error: err.message - }); - } - - const buffer = req.file.buffer; - - // Optional ClamAV Malware Scan - if (process.env.ENABLE_MALWARE_SCAN === 'true') { - try { - const isClean = await scanWithClamAV(buffer); - if (!isClean) { - return res.status(400).json({ error: 'File contains malware' }); - // 1. Validate file type (already done by multer) - const fileExt = path.extname(req.file.originalname).toLowerCase(); - if (!ALLOWED_EXTENSIONS.includes(fileExt) && - !ALLOWED_MIME_TYPES.includes(req.file.mimetype)) { - return res.status(400).json({ - success: false, - error: `Invalid file type. Only CSV files are allowed. Received: ${req.file.mimetype}` - }); - } - - // 2. Optional: Scan for malware - if (process.env.ENABLE_MALWARE_SCAN === 'true') { - try { - await scanForMalware(req.file.buffer, req.file.originalname); - } catch (scanError) { - return res.status(400).json({ - success: false, - error: `Security scan failed: ${scanError.message}` - }); - } - } - - // 3. Parse CSV content - const csvContent = req.file.buffer.toString('utf8'); - - // Basic CSV parsing (handle quoted fields) - const lines = csvContent.split('\n').filter(line => line.trim()); - if (lines.length === 0) { - return res.status(400).json({ - success: false, - error: 'CSV file is empty' - }); - } - - // Parse headers - const headers = parseCSVLine(lines[0]); - if (headers.length === 0) { - return res.status(400).json({ - success: false, - error: 'Invalid CSV headers' - }); - } - - // Parse rows and sanitize - const rows = []; - const validationErrors = []; - - for (let i = 1; i < lines.length; i++) { - const values = parseCSVLine(lines[i]); - const row = {}; - - headers.forEach((header, index) => { - const value = values[index] || ''; - // Sanitize each cell - row[header] = sanitizeCSVCell(value); - }); - - rows.push(row); - } - - // 4. Validate CSV structure - const validationResults = validateCSVContent(rows, headers); - if (validationResults.length > 0) { - return res.status(400).json({ - success: false, - errors: validationResults - }); - } - } catch (scanError) { - console.error('Malware scan failed:', scanError); - return res.status(500).json({ error: 'Malware scan service unavailable' }); - } - } - - const fileContent = buffer.toString('utf-8'); - if (!fileContent.trim()) { - return res.status(400).json({ error: 'CSV file is empty' }); - } - - const lines = fileContent.split(/\r?\n/).filter(line => line.trim()); - if (lines.length === 0) { - return res.status(400).json({ error: 'CSV file is empty' }); - } - - // Custom row count limit check from env - const maxRows = parseInt(process.env.MAX_CSV_ROWS, 10) || 100000; - if (lines.length - 1 > maxRows) { - return res.status(413).json({ error: 'File too large (too many rows)' }); - } - - // Parse headers and normalize/trim - const rawHeaders = parseCSVLine(lines[0]); - const normalizedHeaders = rawHeaders.map(h => h.trim().toLowerCase()); - - // Must contain "text" or "message" column - const hasText = normalizedHeaders.includes('text') || normalizedHeaders.includes('message'); - if (!hasText) { - return res.status(400).json({ - error: 'Invalid CSV format', - errors: ['CSV file must contain a "text" or "message" column'] - }); - } - - const rows = []; - for (let i = 1; i < lines.length; i++) { - const parsedLine = parseCSVLine(lines[i]); - const rowObj = {}; - rawHeaders.forEach((header, index) => { - const val = parsedLine[index] !== undefined ? parsedLine[index] : ''; - // Store sanitized values in the row object keyed by the parsed header - rowObj[header.trim()] = sanitizeCSVCell(val); - }); - rows.push(rowObj); - - // Yield to the event loop every 500 rows to prevent blocking (DoS) - if (i % 500 === 0) { - await new Promise(resolve => setImmediate(resolve)); -/** - * Parse CSV line handling quoted fields - */ -function parseCSVLine(line) { - const values = []; - let current = ''; - let inQuotes = false; - - for (let i = 0; i < line.length; i++) { - const char = line[i]; - - if (char === '"') { - if (inQuotes && line[i + 1] === '"') { - // Escaped quote - current += '"'; - i++; - } else { - inQuotes = !inQuotes; - } - } - } - - values.push(current.trim()); - return values; -} - - req.parsedCSV = { - headers: rawHeaders.map(h => h.trim()), - rows: rows, - totalRows: rows.length, - filename: req.file.originalname, - size: req.file.size - }; - - next(); - }); -}; - -module.exports = { - parseCSVLine, - sanitizeCSVCell, - validateCSVUpload -}; diff --git a/backend/middleware/rateLimiter.js.bak b/backend/middleware/rateLimiter.js.bak deleted file mode 100644 index db2134cc..00000000 --- a/backend/middleware/rateLimiter.js.bak +++ /dev/null @@ -1,361 +0,0 @@ -const rateLimit = require('express-rate-limit'); -const { ipKeyGenerator } = require('express-rate-limit'); -const RedisStore = require('rate-limit-redis'); -const redis = require('redis'); - -// ============================================ -// REDIS CONFIGURATION (Optional but recommended) -// ============================================ -let redisClient = null; -let store = undefined; - -// Try to connect to Redis if configured -if (process.env.REDIS_URL) { - try { - redisClient = redis.createClient({ - url: process.env.REDIS_URL, - socket: { - reconnectStrategy: (retries) => { - if (retries > 10) { - console.warn('⚠️ Redis connection failed, falling back to memory store'); - return false; - } - return Math.min(retries * 100, 3000); - } - } - }); - - redisClient.on('error', (err) => { - console.warn('⚠️ Redis error:', err.message); - store = undefined; - }); - - redisClient.connect().then(() => { - console.log('✅ Redis connected for rate limiting'); - store = new RedisStore({ - sendCommand: (...args) => redisClient.sendCommand(args), - }); - }).catch(() => { - console.warn('⚠️ Redis connection failed, using memory store'); - store = undefined; - }); - } catch (error) { - console.warn('⚠️ Redis not available, using memory store'); - store = undefined; - } -} - -// ============================================ -// 1. STRICT AUTH LIMITERS (Your Existing Code) -// ============================================ - -/** - * Login Rate Limiter - * Limits login attempts to prevent brute force attacks - */ -const loginLimiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 5, // 5 attempts per window - store: store, - message: { - success: false, - message: 'Too many login attempts from this IP, please try again after 15 minutes.' - }, - standardHeaders: true, - legacyHeaders: false, - skipSuccessfulRequests: true, // Don't count successful logins - keyGenerator: (req) => { - // Rate limit by email or IP - return req.body.email || req.ip || req.connection.remoteAddress; - }, -}); - -/** - * Registration Rate Limiter - * Prevents mass account creation - */ -const registerLimiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 5, // 5 registrations per window - store: store, - message: { - success: false, - message: 'Too many registration attempts from this IP, please try again after 15 minutes.' - }, - standardHeaders: true, - legacyHeaders: false, -}); - -/** - * Password Reset Rate Limiter - * Prevents abuse of password reset functionality - */ -const resetLimiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 3, // 3 reset requests per window - store: store, - message: { - success: false, - message: 'Too many password reset requests, please try again later.' - }, - standardHeaders: true, - legacyHeaders: false, - keyGenerator: (req) => { - // Rate limit by email or IP - return req.body.email || req.ip || req.connection.remoteAddress; - }, -}); - -/** - * General API Rate Limiter - * Prevents API abuse and DoS attacks - */ -const apiLimiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 100, // 100 requests per window - store: store, - message: { - success: false, - message: 'Too many API requests from this IP, please try again after 15 minutes.' - }, - standardHeaders: true, - legacyHeaders: false, -}); - -// ============================================ -// 2. MAINTAINER'S LIMITERS (Incoming Code) -// ============================================ - -/** - * Chat Rate Limiter - * Prevents spam in chat endpoints - */ -const chatLimiter = rateLimit({ - windowMs: 60 * 1000, // 1 minute - max: 15, // 15 messages per minute - store: store, - message: { - success: false, - error: "Too many chat requests. Please slow down." - }, - standardHeaders: true, - legacyHeaders: false, - keyGenerator: (req) => { - // Rate limit by user ID if authenticated, otherwise by IP - return req.user?.id || req.ip || req.connection.remoteAddress; - }, -}); - -/** - * Predict Rate Limiter - * Prevents abuse of ML prediction endpoint - */ -const PREDICT_WINDOW_MS = Number(process.env.PREDICT_RATE_LIMIT_WINDOW_MS) || 60 * 1000; -const PREDICT_MAX = Number(process.env.PREDICT_RATE_LIMIT_MAX) || 30; - -const predictLimiter = rateLimit({ - windowMs: PREDICT_WINDOW_MS, - max: PREDICT_MAX, - store: store, - standardHeaders: true, - legacyHeaders: false, - keyGenerator: (req) => { - // Rate limit by user ID if authenticated, otherwise by IP - return req.user?.id || req.ip || req.connection.remoteAddress; - }, - handler: (req, res, next, options) => { - const retryAfterSeconds = Math.ceil(options.windowMs / 1000); - res.status(429).json({ - success: false, - error: "Too many predict requests. Please slow down.", - retryAfter: retryAfterSeconds, - limit: options.max, - remaining: 0, - resetTime: new Date(Date.now() + options.windowMs).toISOString() - }); - } -}); - -// ============================================ -// 3. OTP & VERIFICATION LIMITERS (NEW) -// ============================================ - -/** - * OTP Request Rate Limiter - * Prevents SMS/Email bombing attacks - * Stricter limits for security - */ -const otpLimiter = rateLimit({ - windowMs: 5 * 60 * 1000, // 5 minutes - max: 3, // Only 3 OTP requests per 5 minutes - store: store, - message: { - success: false, - error: 'Too many OTP requests. Please wait 5 minutes.', - retryAfter: 5 * 60 // 5 minutes in seconds - }, - standardHeaders: true, - legacyHeaders: false, - keyGenerator: (req) => { - // Rate limit by email or phone or IP - return req.body.email || req.body.phone || req.ip || req.connection.remoteAddress; - }, - handler: (req, res, next, options) => { - const retryAfterSeconds = Math.ceil(options.windowMs / 1000); - res.status(429).json({ - success: false, - error: 'Rate limit exceeded. Maximum 3 OTP requests per 5 minutes.', - retryAfter: retryAfterSeconds, - limit: options.max, - remaining: 0, - resetTime: new Date(Date.now() + options.windowMs).toISOString() - }); - } -}); - -/** - * OTP Verification Rate Limiter - * Prevents brute force OTP attempts - */ -const verificationLimiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 5, // Max 5 verification attempts - store: store, - message: { - success: false, - error: 'Too many verification attempts. Please try again later.' - }, - standardHeaders: true, - legacyHeaders: false, - keyGenerator: (req) => { - // Rate limit by email, phone, or IP - return req.body.email || req.body.phone || req.ip || req.connection.remoteAddress; - }, - skipSuccessfulRequests: true, // Don't count successful verifications - handler: (req, res, next, options) => { - const retryAfterSeconds = Math.ceil(options.windowMs / 1000); - res.status(429).json({ - success: false, - error: 'Too many verification attempts. Please try again in 15 minutes.', - retryAfter: retryAfterSeconds, - limit: options.max, - remaining: 0 - }); - } -}); - -/** - * Bulk Predict Rate Limiter - * Prevents abuse of bulk prediction endpoint - */ -const bulkPredictLimiter = rateLimit({ - windowMs: 60 * 60 * 1000, // 1 hour - max: 10, // Max 10 bulk predictions per hour - store: store, - message: { - success: false, - error: 'Too many bulk prediction requests. Please wait 1 hour.' - }, - standardHeaders: true, - legacyHeaders: false, - keyGenerator: (req) => { - return req.user?.id || req.ip || req.connection.remoteAddress; - }, -}); - -/** - * Export Rate Limiter - * Prevents abuse of data export endpoints - */ -const exportLimiter = rateLimit({ - windowMs: 60 * 60 * 1000, // 1 hour - max: 5, // Max 5 exports per hour - store: store, - message: { - success: false, - error: 'Too many export requests. Please wait 1 hour.' - }, - standardHeaders: true, - legacyHeaders: false, - keyGenerator: (req) => { - return req.user?.id || req.ip || req.connection.remoteAddress; - }, -}); - -/** - * Feedback Rate Limiter - * Prevents spam feedback submissions - */ -const feedbackLimiter = rateLimit({ - windowMs: 60 * 1000, // 1 minute - max: 10, // Max 10 feedback submissions per minute - store: store, - message: { - success: false, - error: 'Too many feedback submissions. Please slow down.' - }, - standardHeaders: true, - legacyHeaders: false, - keyGenerator: (req) => { - return req.user?.id || req.ip || req.connection.remoteAddress; - }, -}); - -// ============================================ -// 4. ENVIRONMENT-SPECIFIC CONFIGURATIONS -// ============================================ - -/** - * Development environment - less strict limits - * Production environment - stricter limits - */ -const isProduction = process.env.NODE_ENV === 'production'; -const isDevelopment = process.env.NODE_ENV === 'development'; - -// Dynamic limiters based on environment -const getLimiterConfig = (baseConfig) => { - if (isDevelopment) { - // Less strict in development - return { - ...baseConfig, - max: baseConfig.max * 2, - windowMs: baseConfig.windowMs / 2, - }; - } - return baseConfig; -}; - -// ============================================ -// 5. EXPORT ALL LIMITERS -// ============================================ - -module.exports = { - // Auth limiters - loginLimiter, - registerLimiter, - resetLimiter, - apiLimiter, - - // Feature limiters - chatLimiter, - predictLimiter, - bulkPredictLimiter, - exportLimiter, - feedbackLimiter, - - // OTP limiters - otpLimiter, - verificationLimiter, - - // Configuration values - PREDICT_MAX, - PREDICT_WINDOW_MS, - - // Redis client (for external use) - redisClient, - - // Utility functions - isProduction, - isDevelopment, - getLimiterConfig -}; \ No newline at end of file diff --git a/backend/middleware/validation.middleware.js b/backend/middleware/validation.middleware.js deleted file mode 100644 index 98f5d672..00000000 --- a/backend/middleware/validation.middleware.js +++ /dev/null @@ -1,34 +0,0 @@ -const Joi = require('joi'); - -class ValidationMiddleware { - /** - * Validate request body against schema - */ - static validate(schema) { - return (req, res, next) => { - const { error, value } = schema.validate(req.body, { - abortEarly: false, - stripUnknown: true - }); - - if (error) { - const errors = error.details.map(detail => ({ - field: detail.path.join('.'), - message: detail.message - })); - - return res.status(400).json({ - success: false, - error: 'Validation failed', - details: errors - }); - } - - // Replace req.body with validated value - req.body = value; - next(); - }; - } -} - -module.exports = ValidationMiddleware; \ No newline at end of file diff --git a/backend/routes/Tuple[str b/backend/routes/Tuple[str deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/routes/bool b/backend/routes/bool deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/routes/dict b/backend/routes/dict deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/routes/prediction.route.js b/backend/routes/prediction.route.js deleted file mode 100644 index b79679ab..00000000 --- a/backend/routes/prediction.route.js +++ /dev/null @@ -1,41 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const auth = require('../middleware/auth'); - -// GET /api/predictions/stats -router.get('/stats', auth, async (req, res) => { - try { - const userId = req.user.id; - const today = new Date(); - today.setHours(0, 0, 0, 0); - - // Get predictions from database - const predictions = await Prediction.find({ userId }); - - // Calculate stats - const total = predictions.length; - const todayCount = predictions.filter(p => - new Date(p.createdAt) >= today - ).length; - - // Spam vs Ham breakdown - const spamCount = predictions.filter(p => - p.result === 'spam' || p.result === 'smishing' - ).length; - const hamCount = predictions.filter(p => - p.result === 'ham' || p.result === 'safe' - ).length; - - res.json({ - today: todayCount, - total: total, - spamCount: spamCount, - hamCount: hamCount - }); - } catch (error) { - console.error('Stats error:', error); - res.status(500).json({ error: 'Failed to fetch stats' }); - } -}); - -module.exports = router; \ No newline at end of file diff --git a/backend/tests/fileValidation.test.js b/backend/tests/fileValidation.test.js deleted file mode 100644 index dda0ac2e..00000000 --- a/backend/tests/fileValidation.test.js +++ /dev/null @@ -1,279 +0,0 @@ -const test = require('node:test'); -const assert = require('node:assert'); -const express = require('express'); -const multer = require('multer'); -const { validateCSVUpload, sanitizeCSVCell, parseCSVLine } = require('../middleware/fileValidation'); - -// ============================================ -// TEST HELPERS -// ============================================ - -function createTestApp() { - const app = express(); - app.post('/test-upload', validateCSVUpload, (req, res) => { - res.json({ - success: true, - data: req.parsedCSV - }); - }); - return app; -} - -function createFormData(content, filename = 'test.csv') { - const boundary = '----testboundary'; - const parts = [ - `--${boundary}`, - `Content-Disposition: form-data; name="file"; filename="${filename}"`, - 'Content-Type: text/csv', - '', - content, - `--${boundary}--` - ]; - return { - buffer: Buffer.from(parts.join('\r\n')), - headers: { - 'Content-Type': `multipart/form-data; boundary=${boundary}`, - } - }; -} - -// ============================================ -// TESTS -// ============================================ - -test('sanitizeCSVCell: should neutralize formula injection', () => { - const testCases = [ - { input: '=cmd|"/C calc"', expected: "'=cmd|\"/C calc\"" }, - { input: '=HYPERLINK("http://evil.com")', expected: "'=HYPERLINK(\"http://evil.com\")" }, - { input: '=DDE("cmd";"/C calc")', expected: "'=DDE(\"cmd\";\"/C calc\")" }, - { input: '=system("calc")', expected: "'=system(\"calc\")" }, - { input: '=shell("calc")', expected: "'=shell(\"calc\")" }, - { input: '=execute("calc")', expected: "'=execute(\"calc\")" }, - { input: '+cmd|"/C calc"', expected: "'+cmd|\"/C calc\"" }, - { input: '@cmd|"/C calc"', expected: "'@cmd|\"/C calc\"" }, - { input: '-cmd|"/C calc"', expected: "'-cmd|\"/C calc\"" }, - { input: 'Normal text', expected: 'Normal text' }, - { input: '12345', expected: '12345' }, - { input: 'Hello, World!', expected: 'Hello, World!' } - ]; - - testCases.forEach(({ input, expected }) => { - const result = sanitizeCSVCell(input); - assert.strictEqual(result, expected, `Failed for input: ${input}`); - }); -}); - -test('sanitizeCSVCell: should escape XSS vectors', () => { - const testCases = [ - { input: '', expected: '<script>alert("xss")</script>' }, - { input: '