const { describe, it, beforeEach, afterEach, before, after } = require('mocha'); const { expect } = require('chai'); const sinon = require('sinon'); const jwt = require('jsonwebtoken'); const { setupTestEnvironment, teardownTestEnvironment, cleanDatabase, createTestUser, createTestTenant, createTestDevice, generateTestToken } = require('../setup'); describe('Security Tests', () => { let models, sequelize; before(async () => { ({ models, sequelize } = await setupTestEnvironment()); }); after(async () => { await teardownTestEnvironment(); }); beforeEach(async () => { await cleanDatabase(); }); describe('Authentication Security', () => { it('should prevent JWT token manipulation', async () => { const tenant = await createTestTenant(); const user = await createTestUser({ tenant_id: tenant.id }); const validToken = generateTestToken(user, tenant); const [header, payload, signature] = validToken.split('.'); // Test various token manipulation attempts const manipulationTests = [ { name: 'Modified payload', token: header + '.' + Buffer.from(JSON.stringify({ ...JSON.parse(Buffer.from(payload, 'base64').toString()), role: 'admin' // Attempt privilege escalation })).toString('base64') + '.' + signature }, { name: 'Modified signature', token: header + '.' + payload + '.' + 'tampered_signature' }, { name: 'Wrong algorithm', token: jwt.sign({ userId: user.id, tenantId: tenant.id }, 'secret', { algorithm: 'HS256' }) // Different algorithm }, { name: 'Expired token', token: jwt.sign({ userId: user.id, tenantId: tenant.id, exp: Math.floor(Date.now() / 1000) - 3600 // Expired 1 hour ago }, process.env.JWT_SECRET || 'test-secret') } ]; for (const test of manipulationTests) { try { const decoded = jwt.verify(test.token, process.env.JWT_SECRET || 'test-secret'); // If we get here, the token was accepted when it shouldn't be if (test.name === 'Wrong algorithm') { // This might be valid depending on configuration continue; } // Token should have been rejected but wasn't - this is unexpected throw new Error(`Token manipulation test "${test.name}" should have failed but was accepted`); } catch (error) { // Expected behavior - token should be rejected if (error.message && error.message.includes('should have failed but was accepted')) { throw error; // Re-throw unexpected success } expect(error.name).to.be.oneOf(['JsonWebTokenError', 'TokenExpiredError', 'NotBeforeError']); } } }); it('should enforce tenant boundaries in JWT tokens', async () => { const tenant1 = await createTestTenant({ slug: 'tenant1' }); const tenant2 = await createTestTenant({ slug: 'tenant2' }); const user1 = await createTestUser({ tenant_id: tenant1.id }); const user2 = await createTestUser({ tenant_id: tenant2.id }); // Create device for tenant1 const device1 = await createTestDevice({ id: 111, tenant_id: tenant1.id, is_approved: true }); // User from tenant2 tries to access tenant1's device const crossTenantToken = generateTestToken(user2, tenant2); // Simulate middleware that would check tenant access const checkTenantAccess = (token, targetTenantId) => { const decoded = jwt.verify(token, process.env.JWT_SECRET || 'test-secret'); return decoded.tenantId === targetTenantId; }; const hasAccess = checkTenantAccess(crossTenantToken, tenant1.id); expect(hasAccess).to.be.false; // User from tenant1 should have access to their own tenant const validToken = generateTestToken(user1, tenant1); const hasValidAccess = checkTenantAccess(validToken, tenant1.id); expect(hasValidAccess).to.be.true; }); it('should handle brute force login attempts', async () => { const tenant = await createTestTenant(); const user = await createTestUser({ tenant_id: tenant.id, username: 'brutetest', password: 'SecurePassword123!' }); // Mock rate limiting storage const rateLimitStore = new Map(); const checkRateLimit = (identifier, maxAttempts = 5, windowMs = 15 * 60 * 1000) => { const now = Date.now(); const attempts = rateLimitStore.get(identifier) || { count: 0, resetTime: now + windowMs }; if (now > attempts.resetTime) { // Reset window attempts.count = 0; attempts.resetTime = now + windowMs; } attempts.count++; rateLimitStore.set(identifier, attempts); return attempts.count <= maxAttempts; }; // Simulate multiple failed login attempts for (let i = 0; i < 10; i++) { const allowed = checkRateLimit('brutetest'); if (i < 5) { expect(allowed).to.be.true; } else { expect(allowed).to.be.false; // Should be blocked after 5 attempts } } }); }); describe('Authorization Security', () => { it('should prevent privilege escalation attempts', async () => { const tenant = await createTestTenant(); const regularUser = await createTestUser({ tenant_id: tenant.id, role: 'user' }); const adminUser = await createTestUser({ tenant_id: tenant.id, role: 'admin' }); // Test role-based access control const checkPermission = (user, action) => { const permissions = { 'user': ['view_detections', 'view_devices'], 'admin': ['view_detections', 'view_devices', 'manage_devices', 'manage_users', 'view_system'], 'system_admin': ['*'] // All permissions }; const userPermissions = permissions[user.role] || []; return userPermissions.includes(action) || userPermissions.includes('*'); }; // Regular user should not have admin permissions expect(checkPermission(regularUser, 'manage_devices')).to.be.false; expect(checkPermission(regularUser, 'manage_users')).to.be.false; expect(checkPermission(regularUser, 'view_system')).to.be.false; // But should have basic permissions expect(checkPermission(regularUser, 'view_detections')).to.be.true; expect(checkPermission(regularUser, 'view_devices')).to.be.true; // Admin should have admin permissions expect(checkPermission(adminUser, 'manage_devices')).to.be.true; expect(checkPermission(adminUser, 'manage_users')).to.be.true; }); it('should enforce IP address restrictions', async () => { const tenant = await createTestTenant({ ip_restrictions: '192.168.1.0/24,10.0.0.0/8' }); const checkIPRestriction = (clientIP, allowedRanges) => { if (!allowedRanges) return true; const isIPInRange = (ip, range) => { if (range.includes('/')) { // CIDR notation const [network, prefixLength] = range.split('/'); const prefix = parseInt(prefixLength); // Simplified check for testing if (prefix === 24) { const networkPrefix = network.substring(0, network.lastIndexOf('.')); const ipPrefix = ip.substring(0, ip.lastIndexOf('.')); return networkPrefix === ipPrefix; } if (prefix === 8) { const networkPrefix = network.split('.')[0]; const ipPrefix = ip.split('.')[0]; return networkPrefix === ipPrefix; } } return ip === range; }; const ranges = allowedRanges.split(','); return ranges.some(range => isIPInRange(clientIP, range.trim())); }; const allowedIPs = [ '192.168.1.100', '192.168.1.50', '10.0.0.15', '10.5.3.100' ]; const blockedIPs = [ '203.0.113.1', // External IP '172.16.0.1', // Different private range '192.168.2.100' // Wrong subnet ]; allowedIPs.forEach(ip => { const result = checkIPRestriction(ip, tenant.ip_restrictions); console.log(`Testing allowed IP ${ip} against ${tenant.ip_restrictions}: ${result}`); expect(result).to.be.true; }); blockedIPs.forEach(ip => { const result = checkIPRestriction(ip, tenant.ip_restrictions); console.log(`Testing blocked IP ${ip} against ${tenant.ip_restrictions}: ${result}`); expect(result).to.be.false; }); }); it('should prevent unauthorized data modification', async () => { const tenant1 = await createTestTenant(); const tenant2 = await createTestTenant(); const user1 = await createTestUser({ tenant_id: tenant1.id, role: 'admin' }); const user2 = await createTestUser({ tenant_id: tenant2.id, role: 'admin' }); const device1 = await createTestDevice({ id: 123, tenant_id: tenant1.id }); // User2 attempts to modify device belonging to tenant1 const unauthorizedUpdate = async () => { return await models.Device.update( { name: 'Hacked Device' }, { where: { id: device1.id, tenant_id: user2.tenant_id // Wrong tenant } } ); }; const result = await unauthorizedUpdate(); expect(result[0]).to.equal(0); // No rows affected // Verify device was not modified const device = await models.Device.findByPk(device1.id); expect(device.name).to.not.equal('Hacked Device'); }); }); describe('Input Validation Security', () => { it('should prevent SQL injection attempts', async () => { const tenant = await createTestTenant(); const device = await createTestDevice({ tenant_id: tenant.id }); // SQL injection payloads const injectionPayloads = [ "'; DROP TABLE drone_detections; --", "' OR '1'='1", "1; DELETE FROM devices WHERE 1=1; --", "' UNION SELECT * FROM users --" ]; for (const payload of injectionPayloads) { try { // Attempt to use payload in various contexts await models.DroneDetection.findAll({ where: { tenant_id: tenant.id, // Using parameterized queries should prevent injection drone_id: payload } }); // The query should execute safely without SQL injection // (Sequelize uses parameterized queries by default) } catch (error) { // If there's an error, it should be a validation error, not a SQL error expect(error.name).to.not.include('SQL'); } } }); it('should validate and sanitize detection data', async () => { const tenant = await createTestTenant(); const device = await createTestDevice({ tenant_id: tenant.id, is_approved: true }); const maliciousInputs = [ { name: 'XSS attempt in coordinates', data: { device_id: device.id, geo_lat: '', geo_lon: 18.0686, device_timestamp: Date.now(), drone_type: 2 } }, { name: 'Extremely large coordinates', data: { device_id: device.id, geo_lat: 999999.999999, geo_lon: -999999.999999, device_timestamp: Date.now(), drone_type: 2 } }, { name: 'Invalid data types', data: { device_id: device.id, geo_lat: null, geo_lon: undefined, device_timestamp: 'invalid_timestamp', drone_type: 'invalid_type' } }, { name: 'Buffer overflow attempt', data: { device_id: device.id, geo_lat: 59.3293, geo_lon: 18.0686, device_timestamp: Date.now(), drone_type: 2, additional_data: 'A'.repeat(10000) // Very long string } } ]; for (const test of maliciousInputs) { try { await models.DroneDetection.create({ ...test.data, tenant_id: tenant.id }); // If creation succeeds, verify data was sanitized const detection = await models.DroneDetection.findOne({ where: { device_id: device.id }, order: [['id', 'DESC']] }); if (detection) { // Coordinates should be valid numbers if (detection.geo_lat !== null) { expect(detection.geo_lat).to.be.a('number'); expect(detection.geo_lat).to.be.within(-90, 90); } if (detection.geo_lon !== null) { expect(detection.geo_lon).to.be.a('number'); expect(detection.geo_lon).to.be.within(-180, 180); } } } catch (error) { // Expected for invalid data - should be validation error expect(error.name).to.be.oneOf(['SequelizeValidationError', 'SequelizeDatabaseError']); } } }); it('should prevent path traversal attacks', async () => { // Test potential file path manipulation const pathTraversalPayloads = [ '../../../etc/passwd', '..\\..\\..\\windows\\system32\\config\\sam', '/etc/shadow', 'C:\\Windows\\System32\\drivers\\etc\\hosts', '%2e%2e%2f%2e%2e%2f%2e%2e%2fbootini', // URL encoded '....//....//....//etc/passwd' ]; pathTraversalPayloads.forEach(payload => { // Test file path validation function const isValidPath = (path) => { // Should reject paths with traversal attempts return !path.includes('..') && !path.includes('%2e') && !path.startsWith('/') && !path.match(/^[a-zA-Z]:\\/); }; expect(isValidPath(payload)).to.be.false; }); // Valid paths should pass const validPaths = [ 'device_logs.txt', 'reports/detection_summary.pdf', 'data/export.csv' ]; validPaths.forEach(path => { const isValidPath = (path) => { return !path.includes('..') && !path.includes('%2e') && !path.startsWith('/') && !path.match(/^[a-zA-Z]:\\/); }; expect(isValidPath(path)).to.be.true; }); }); }); describe('Data Protection Security', () => { it('should protect sensitive data in database', async () => { const tenant = await createTestTenant(); const user = await createTestUser({ tenant_id: tenant.id, username: 'testuser', email: 'test@example.com', password: 'SecurePassword123!' }); // Verify password is hashed, not stored in plain text expect(user.password_hash).to.exist; expect(user.password_hash).to.not.equal('SecurePassword123!'); expect(user.password_hash.length).to.be.greaterThan(20); // Hashed passwords are longer // Verify sensitive fields are not exposed in JSON const userJSON = user.toJSON(); expect(userJSON.password_hash).to.be.undefined; // Should be hidden expect(userJSON.username).to.exist; // Public fields should remain }); it('should enforce data retention policies', async () => { const tenant = await createTestTenant(); const device = await createTestDevice({ tenant_id: tenant.id }); // Create old detections (simulate 1 year old data) const oldDetections = []; const oneYearAgo = new Date(); oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1); for (let i = 0; i < 10; i++) { oldDetections.push({ device_id: device.id, tenant_id: tenant.id, geo_lat: 59.3293, geo_lon: 18.0686, device_timestamp: oneYearAgo, drone_type: 2, rssi: -60, freq: 2400, drone_id: 1000 + i, threat_level: 'low', createdAt: oneYearAgo, updatedAt: oneYearAgo }); } await models.DroneDetection.bulkCreate(oldDetections); // Create recent detections const recentDetections = []; for (let i = 0; i < 5; i++) { recentDetections.push({ device_id: device.id, tenant_id: tenant.id, geo_lat: 59.3293, geo_lon: 18.0686, device_timestamp: new Date(), drone_type: 2, rssi: -60, freq: 2400, drone_id: 2000 + i, threat_level: 'medium', createdAt: new Date(), updatedAt: new Date() }); } await models.DroneDetection.bulkCreate(recentDetections); // Simulate data retention cleanup (delete data older than 6 months) const sixMonthsAgo = new Date(); sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6); const deleteResult = await models.DroneDetection.destroy({ where: { tenant_id: tenant.id, createdAt: { [models.Sequelize.Op.lt]: sixMonthsAgo } } }); expect(deleteResult).to.equal(10); // Should delete old records // Verify recent data remains const remainingDetections = await models.DroneDetection.findAll({ where: { tenant_id: tenant.id } }); expect(remainingDetections).to.have.length(5); }); it('should anonymize exported data', async () => { const tenant = await createTestTenant(); const user = await createTestUser({ tenant_id: tenant.id }); const device = await createTestDevice({ tenant_id: tenant.id, geo_lat: 59.3293, geo_lon: 18.0686 }); const detection = await models.DroneDetection.create({ device_id: device.id, tenant_id: tenant.id, geo_lat: 59.3293, geo_lon: 18.0686, device_timestamp: new Date(), drone_type: 2, rssi: -60, freq: 2400, drone_id: 12345, threat_level: 'medium' }); // Function to anonymize data for export const anonymizeForExport = (data) => { return { ...data, // Remove exact coordinates, use general area geo_lat: Math.round(data.geo_lat * 100) / 100, // Reduce precision geo_lon: Math.round(data.geo_lon * 100) / 100, // Remove device-specific identifiers device_id: null, // Hash drone ID instead of exposing it drone_id_hash: require('crypto').createHash('sha256').update(data.drone_id.toString()).digest('hex').substring(0, 8) }; }; const anonymized = anonymizeForExport(detection.toJSON()); expect(anonymized.device_id).to.be.null; expect(anonymized.drone_id_hash).to.exist; expect(anonymized.drone_id_hash).to.not.equal(detection.drone_id); expect(anonymized.geo_lat).to.be.lessThan(detection.geo_lat + 0.005); // Reduced precision }); }); describe('API Security', () => { it('should prevent API abuse and rate limiting bypass', async () => { const device = await createTestDevice({ is_approved: true }); // Simulate rate limiting for detection endpoint const requestCounts = new Map(); const checkRateLimit = (deviceId, maxPerMinute = 60) => { const now = Date.now(); const windowStart = Math.floor(now / 60000) * 60000; // 1-minute windows const key = `${deviceId}-${windowStart}`; const count = requestCounts.get(key) || 0; requestCounts.set(key, count + 1); return count < maxPerMinute; }; // Test normal usage - should be allowed for (let i = 0; i < 50; i++) { expect(checkRateLimit(device.id)).to.be.true; } // Test excessive usage - should be blocked for (let i = 0; i < 20; i++) { const allowed = checkRateLimit(device.id); if (i < 10) { expect(allowed).to.be.true; } else { expect(allowed).to.be.false; } } }); it('should validate API request size limits', async () => { const validateRequestSize = (data, maxSizeKB = 100) => { const dataSize = JSON.stringify(data).length; const maxSizeBytes = maxSizeKB * 1024; return dataSize <= maxSizeBytes; }; // Normal request should pass const normalRequest = { device_id: 123, geo_lat: 59.3293, geo_lon: 18.0686, device_timestamp: Date.now(), drone_type: 2 }; expect(validateRequestSize(normalRequest)).to.be.true; // Oversized request should fail const oversizedRequest = { ...normalRequest, malicious_payload: 'A'.repeat(200 * 1024) // 200KB of data }; expect(validateRequestSize(oversizedRequest)).to.be.false; }); it('should prevent CSRF attacks', async () => { const tenant = await createTestTenant(); const user = await createTestUser({ tenant_id: tenant.id }); // Generate CSRF token const generateCSRFToken = (userId, secret = 'csrf-secret') => { return require('crypto') .createHmac('sha256', secret) .update(`${userId}-${Date.now()}`) .digest('hex'); }; // Validate CSRF token const validateCSRFToken = (token, userId, secret = 'csrf-secret', maxAge = 3600000) => { try { // In a real implementation, you'd store token metadata // This is a simplified validation return token && token.length === 64; // Valid format } catch (error) { return false; } }; const validToken = generateCSRFToken(user.id); expect(validateCSRFToken(validToken, user.id)).to.be.true; // Invalid tokens should fail expect(validateCSRFToken('invalid_token', user.id)).to.be.false; expect(validateCSRFToken(null, user.id)).to.be.false; }); }); });