diff --git a/client/src/App.jsx b/client/src/App.jsx index cc30a89..888141f 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -12,6 +12,7 @@ import Detections from './pages/Detections'; import Alerts from './pages/Alerts'; import Debug from './pages/Debug'; import Settings from './pages/Settings'; +import SecurityLogs from './pages/SecurityLogs'; import Login from './pages/Login'; import Register from './pages/Register'; import ProtectedRoute from './components/ProtectedRoute'; diff --git a/client/src/pages/SecurityLogs.jsx b/client/src/pages/SecurityLogs.jsx new file mode 100644 index 0000000..152872e --- /dev/null +++ b/client/src/pages/SecurityLogs.jsx @@ -0,0 +1,303 @@ +import React, { useState, useEffect, useContext } from 'react'; +import { AuthContext } from '../contexts/AuthContext'; +import { Card, CardHeader, CardTitle, CardContent } from '../components/ui/card'; +import { Alert, AlertDescription } from '../components/ui/alert'; +import { Badge } from '../components/ui/badge'; +import { formatDistanceToNow } from 'date-fns'; + +const SecurityLogs = () => { + const { user, tenant } = useContext(AuthContext); + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [filters, setFilters] = useState({ + level: 'all', + eventType: 'all', + timeRange: '24h', + search: '' + }); + const [pagination, setPagination] = useState({ + page: 1, + limit: 50, + total: 0 + }); + + useEffect(() => { + // Only allow admins to view security logs + if (!user || user.role !== 'admin') { + setError('Access denied. Only tenant administrators can view security logs.'); + setLoading(false); + return; + } + + loadSecurityLogs(); + }, [filters, pagination.page, user]); + + const loadSecurityLogs = async () => { + if (!user || user.role !== 'admin') return; + + setLoading(true); + try { + const params = new URLSearchParams({ + page: pagination.page, + limit: pagination.limit, + ...filters + }); + + const response = await fetch(`/api/security-logs?${params}`, { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}`, + 'X-Tenant-ID': tenant?.id || '' + } + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + setLogs(data.logs || []); + setPagination(prev => ({ + ...prev, + total: data.total || 0 + })); + } catch (err) { + console.error('Failed to load security logs:', err); + setError(err.message); + } finally { + setLoading(false); + } + }; + + const getLogLevelBadge = (level) => { + const styles = { + 'critical': 'bg-red-500 text-white', + 'high': 'bg-orange-500 text-white', + 'medium': 'bg-yellow-500 text-black', + 'low': 'bg-blue-500 text-white', + 'info': 'bg-gray-500 text-white' + }; + return styles[level] || styles.info; + }; + + const getEventTypeIcon = (eventType) => { + const icons = { + 'failed_login': '🚫', + 'successful_login': '✅', + 'suspicious_activity': '⚠️', + 'country_alert': '🌍', + 'brute_force': '🔨', + 'account_lockout': '🔒', + 'password_reset': '🔄', + 'admin_action': '👤' + }; + return icons[eventType] || '📋'; + }; + + const formatMetadata = (metadata) => { + if (!metadata) return ''; + const items = []; + if (metadata.ip_address) items.push(`IP: ${metadata.ip_address}`); + if (metadata.country) items.push(`Country: ${metadata.country}`); + if (metadata.user_agent) items.push(`Agent: ${metadata.user_agent.substring(0, 50)}...`); + if (metadata.username) items.push(`User: ${metadata.username}`); + return items.join(' | '); + }; + + // Access control check + if (!user || user.role !== 'admin') { + return ( +
+ + + Access denied. Only tenant administrators can view security logs. + + +
+ ); + } + + const totalPages = Math.ceil(pagination.total / pagination.limit); + + return ( +
+
+

Security Logs

+

+ Security events for {tenant?.name || 'your organization'} +

+
+ + {error && ( + + + {error} + + + )} + + {/* Filters */} + + + Filters + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + setFilters(prev => ({ ...prev, search: e.target.value }))} + className="w-full p-2 border rounded-md" + /> +
+
+
+
+ + {/* Security Logs Table */} + + + + Security Events + + {pagination.total} total events + + + + + {loading ? ( +
+
Loading security logs...
+
+ ) : logs.length === 0 ? ( +
+ No security logs found matching your criteria +
+ ) : ( +
+ + + + + + + + + + + + {logs.map((log) => ( + + + + + + + + ))} + +
TimeLevelEventMessageDetails
+
{new Date(log.timestamp).toLocaleString()}
+
+ {formatDistanceToNow(new Date(log.timestamp), { addSuffix: true })} +
+
+ + {log.level.toUpperCase()} + + +
+ {getEventTypeIcon(log.event_type)} + {log.event_type.replace('_', ' ').toUpperCase()} +
+
+
+ {log.message} +
+
+
+ {formatMetadata(log.metadata)} +
+
+
+ )} + + {/* Pagination */} + {totalPages > 1 && ( +
+
+ Page {pagination.page} of {totalPages} +
+
+ + +
+
+ )} +
+
+
+ ); +}; + +export default SecurityLogs; \ No newline at end of file diff --git a/management/src/App.jsx b/management/src/App.jsx index d245cb9..d326ddf 100644 --- a/management/src/App.jsx +++ b/management/src/App.jsx @@ -10,6 +10,7 @@ import Tenants from './pages/Tenants' import TenantUsersPage from './pages/TenantUsersPage' import Users from './pages/Users' import System from './pages/System' +import SecurityLogs from './pages/SecurityLogs' function App() { return ( @@ -29,6 +30,7 @@ function App() { } /> } /> } /> + } /> { { name: t('nav.dashboard'), href: '/dashboard', icon: HomeIcon }, { name: t('nav.tenants'), href: '/tenants', icon: BuildingOfficeIcon }, { name: t('nav.users'), href: '/users', icon: UsersIcon }, + { name: t('nav.security_logs'), href: '/security-logs', icon: ShieldCheckIcon }, { name: t('nav.system'), href: '/system', icon: CogIcon }, ] diff --git a/management/src/pages/SecurityLogs.jsx b/management/src/pages/SecurityLogs.jsx new file mode 100644 index 0000000..26fc600 --- /dev/null +++ b/management/src/pages/SecurityLogs.jsx @@ -0,0 +1,276 @@ +import React, { useState, useEffect } from 'react'; +import { Card, CardHeader, CardTitle, CardContent } from '../components/ui/card'; +import { Alert, AlertDescription } from '../components/ui/alert'; +import { Badge } from '../components/ui/badge'; +import { formatDistanceToNow } from 'date-fns'; + +const SecurityLogs = () => { + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [filters, setFilters] = useState({ + level: 'all', + eventType: 'all', + timeRange: '24h', + search: '' + }); + const [pagination, setPagination] = useState({ + page: 1, + limit: 50, + total: 0 + }); + + useEffect(() => { + loadSecurityLogs(); + }, [filters, pagination.page]); + + const loadSecurityLogs = async () => { + setLoading(true); + try { + const params = new URLSearchParams({ + page: pagination.page, + limit: pagination.limit, + ...filters + }); + + const response = await fetch(`/management/api/security-logs?${params}`, { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('managementToken')}` + } + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + setLogs(data.logs || []); + setPagination(prev => ({ + ...prev, + total: data.total || 0 + })); + } catch (err) { + console.error('Failed to load security logs:', err); + setError(err.message); + } finally { + setLoading(false); + } + }; + + const getLogLevelBadge = (level) => { + const styles = { + 'critical': 'bg-red-500 text-white', + 'high': 'bg-orange-500 text-white', + 'medium': 'bg-yellow-500 text-black', + 'low': 'bg-blue-500 text-white', + 'info': 'bg-gray-500 text-white' + }; + return styles[level] || styles.info; + }; + + const getEventTypeIcon = (eventType) => { + const icons = { + 'failed_login': '🚫', + 'successful_login': '✅', + 'suspicious_activity': '⚠️', + 'country_alert': '🌍', + 'brute_force': '🔨', + 'account_lockout': '🔒', + 'password_reset': '🔄', + 'admin_action': '👤' + }; + return icons[eventType] || '📋'; + }; + + const formatMetadata = (metadata) => { + if (!metadata) return ''; + const items = []; + if (metadata.ip_address) items.push(`IP: ${metadata.ip_address}`); + if (metadata.country) items.push(`Country: ${metadata.country}`); + if (metadata.user_agent) items.push(`Agent: ${metadata.user_agent.substring(0, 50)}...`); + if (metadata.tenant_slug) items.push(`Tenant: ${metadata.tenant_slug}`); + return items.join(' | '); + }; + + const totalPages = Math.ceil(pagination.total / pagination.limit); + + return ( +
+
+

Security Logs

+

Monitor security events across all tenants

+
+ + {error && ( + + + {error} + + + )} + + {/* Filters */} + + + Filters + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + setFilters(prev => ({ ...prev, search: e.target.value }))} + className="w-full p-2 border rounded-md" + /> +
+
+
+
+ + {/* Security Logs Table */} + + + + Security Events + + {pagination.total} total events + + + + + {loading ? ( +
+
Loading security logs...
+
+ ) : logs.length === 0 ? ( +
+ No security logs found matching your criteria +
+ ) : ( +
+ + + + + + + + + + + + {logs.map((log) => ( + + + + + + + + ))} + +
TimeLevelEventMessageDetails
+
{new Date(log.timestamp).toLocaleString()}
+
+ {formatDistanceToNow(new Date(log.timestamp), { addSuffix: true })} +
+
+ + {log.level.toUpperCase()} + + +
+ {getEventTypeIcon(log.event_type)} + {log.event_type.replace('_', ' ').toUpperCase()} +
+
+
+ {log.message} +
+
+
+ {formatMetadata(log.metadata)} +
+
+
+ )} + + {/* Pagination */} + {totalPages > 1 && ( +
+
+ Page {pagination.page} of {totalPages} +
+
+ + +
+
+ )} +
+
+
+ ); +}; + +export default SecurityLogs; \ No newline at end of file diff --git a/management/src/utils/tempTranslations.js b/management/src/utils/tempTranslations.js index 776ba5f..5eedc22 100644 --- a/management/src/utils/tempTranslations.js +++ b/management/src/utils/tempTranslations.js @@ -5,6 +5,7 @@ const translations = { dashboard: 'Dashboard', tenants: 'Tenants', users: 'Users', + security_logs: 'Security Logs', system: 'System' }, navigation: { diff --git a/server/routes/index.js b/server/routes/index.js index 49984a1..8ca6d60 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -17,6 +17,7 @@ const detectionsRoutes = require('./detections'); const droneTypesRoutes = require('./droneTypes'); const tenantDebugRoutes = require('./tenant-debug'); const dataRetentionRoutes = require('./dataRetention'); +const securityLogsRoutes = require('./securityLogs'); // Management portal routes (before API versioning) router.use('/management', managementRoutes); @@ -37,6 +38,7 @@ router.use('/v1/device-health', deviceHealthRoutes); router.use('/v1/detectors', detectorsRoutes); router.use('/v1/detections', detectionsRoutes); router.use('/v1/drone-types', droneTypesRoutes); +router.use('/v1/security-logs', securityLogsRoutes); // Default routes (no version prefix for backward compatibility) router.use('/devices', deviceRoutes); @@ -49,6 +51,7 @@ router.use('/debug', debugRoutes); router.use('/detectors', detectorsRoutes); router.use('/detections', detectionsRoutes); router.use('/drone-types', droneTypesRoutes); +router.use('/security-logs', securityLogsRoutes); router.use('/tenant-debug', tenantDebugRoutes); router.use('/data-retention', dataRetentionRoutes); @@ -67,6 +70,7 @@ router.get('/', (req, res) => { health: '/api/health', 'device-health': '/api/device-health', 'drone-types': '/api/drone-types', + 'security-logs': '/api/security-logs', 'data-retention': '/api/data-retention' }, microservices: { diff --git a/server/routes/management.js b/server/routes/management.js index ced7ce1..641d5a4 100644 --- a/server/routes/management.js +++ b/server/routes/management.js @@ -1561,4 +1561,93 @@ router.get('/audit-logs/summary', requireManagementAuth, async (req, res) => { } }); +// Security logs endpoint - view ALL security logs across tenants +router.get('/security-logs', requireManagementAuth, async (req, res) => { + try { + const { + page = 1, + limit = 50, + level = 'all', + eventType = 'all', + timeRange = '24h', + search = '' + } = req.query; + + const { SecurityLog } = require('../models'); + + // Build where conditions + let whereConditions = {}; + + // Filter by security level + if (level !== 'all') { + whereConditions.level = level; + } + + // Filter by event type + if (eventType !== 'all') { + whereConditions.event_type = eventType; + } + + // Filter by time range + const now = new Date(); + let startTime; + switch (timeRange) { + case '1h': + startTime = new Date(now.getTime() - 60 * 60 * 1000); + break; + case '24h': + startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000); + break; + case '7d': + startTime = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + break; + case '30d': + startTime = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + break; + default: + startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000); + } + + if (timeRange !== 'all') { + whereConditions.timestamp = { [Op.gte]: startTime }; + } + + // Search filter + if (search) { + whereConditions[Op.or] = [ + { message: { [Op.iLike]: `%${search}%` } }, + { 'metadata.ip_address': { [Op.iLike]: `%${search}%` } }, + { 'metadata.username': { [Op.iLike]: `%${search}%` } }, + { 'metadata.tenant_slug': { [Op.iLike]: `%${search}%` } } + ]; + } + + const offset = (parseInt(page) - 1) * parseInt(limit); + + const { rows: logs, count: total } = await SecurityLog.findAndCountAll({ + where: whereConditions, + order: [['timestamp', 'DESC']], + limit: parseInt(limit), + offset: offset + }); + + res.json({ + success: true, + logs, + total, + page: parseInt(page), + limit: parseInt(limit), + totalPages: Math.ceil(total / parseInt(limit)) + }); + + } catch (error) { + console.error('Management: Error retrieving security logs:', error); + res.status(500).json({ + success: false, + message: 'Failed to retrieve security logs', + error: error.message + }); + } +}); + module.exports = router; diff --git a/server/routes/securityLogs.js b/server/routes/securityLogs.js new file mode 100644 index 0000000..e462e95 --- /dev/null +++ b/server/routes/securityLogs.js @@ -0,0 +1,192 @@ +const express = require('express'); +const router = express.Router(); +const { Op } = require('sequelize'); +const { authenticateToken, requireRole } = require('../middleware/auth'); + +// Security logs endpoint - tenant-specific with admin-only access +router.get('/', authenticateToken, requireRole('admin'), async (req, res) => { + try { + const { + page = 1, + limit = 50, + level = 'all', + eventType = 'all', + timeRange = '24h', + search = '' + } = req.query; + + const { SecurityLog } = require('../models'); + + // Build where conditions - only show logs for current tenant + let whereConditions = { + tenant_id: req.user.tenant_id // Ensure tenant isolation + }; + + // Filter by security level + if (level !== 'all') { + whereConditions.level = level; + } + + // Filter by event type + if (eventType !== 'all') { + whereConditions.event_type = eventType; + } + + // Filter by time range + const now = new Date(); + let startTime; + switch (timeRange) { + case '1h': + startTime = new Date(now.getTime() - 60 * 60 * 1000); + break; + case '24h': + startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000); + break; + case '7d': + startTime = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + break; + case '30d': + startTime = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + break; + default: + startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000); + } + + if (timeRange !== 'all') { + whereConditions.timestamp = { [Op.gte]: startTime }; + } + + // Search filter - only search within tenant's logs + if (search) { + whereConditions[Op.or] = [ + { message: { [Op.iLike]: `%${search}%` } }, + { 'metadata.ip_address': { [Op.iLike]: `%${search}%` } }, + { 'metadata.username': { [Op.iLike]: `%${search}%` } } + ]; + } + + const offset = (parseInt(page) - 1) * parseInt(limit); + + const { rows: logs, count: total } = await SecurityLog.findAndCountAll({ + where: whereConditions, + order: [['timestamp', 'DESC']], + limit: parseInt(limit), + offset: offset, + attributes: [ + 'id', + 'timestamp', + 'level', + 'event_type', + 'message', + 'metadata' + ] // Don't expose sensitive internal data + }); + + res.json({ + success: true, + logs, + total, + page: parseInt(page), + limit: parseInt(limit), + totalPages: Math.ceil(total / parseInt(limit)) + }); + + } catch (error) { + console.error('Error retrieving tenant security logs:', error); + res.status(500).json({ + success: false, + message: 'Failed to retrieve security logs', + error: process.env.NODE_ENV === 'development' ? error.message : undefined + }); + } +}); + +// Security logs summary endpoint for dashboard widgets +router.get('/summary', authenticateToken, requireRole('admin'), async (req, res) => { + try { + const { timeRange = '24h' } = req.query; + const { SecurityLog } = require('../models'); + + // Calculate time range + const now = new Date(); + let startTime; + switch (timeRange) { + case '1h': + startTime = new Date(now.getTime() - 60 * 60 * 1000); + break; + case '24h': + startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000); + break; + case '7d': + startTime = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + break; + case '30d': + startTime = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + break; + default: + startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000); + } + + const baseWhere = { + tenant_id: req.user.tenant_id, + timestamp: { [Op.gte]: startTime } + }; + + // Get summary statistics + const [ + totalLogs, + criticalLogs, + highLogs, + failedLogins, + successfulLogins, + countryAlerts, + bruteForceAttempts + ] = await Promise.all([ + SecurityLog.count({ where: baseWhere }), + SecurityLog.count({ where: { ...baseWhere, level: 'critical' } }), + SecurityLog.count({ where: { ...baseWhere, level: 'high' } }), + SecurityLog.count({ where: { ...baseWhere, event_type: 'failed_login' } }), + SecurityLog.count({ where: { ...baseWhere, event_type: 'successful_login' } }), + SecurityLog.count({ where: { ...baseWhere, event_type: 'country_alert' } }), + SecurityLog.count({ where: { ...baseWhere, event_type: 'brute_force' } }) + ]); + + // Get recent critical events + const recentCriticalEvents = await SecurityLog.findAll({ + where: { ...baseWhere, level: { [Op.in]: ['critical', 'high'] } }, + order: [['timestamp', 'DESC']], + limit: 5, + attributes: ['timestamp', 'level', 'event_type', 'message'] + }); + + res.json({ + success: true, + summary: { + period: { + start: startTime.toISOString(), + end: now.toISOString(), + range: timeRange + }, + totals: { + totalLogs, + criticalLogs, + highLogs, + failedLogins, + successfulLogins, + countryAlerts, + bruteForceAttempts + }, + recentCriticalEvents + } + }); + + } catch (error) { + console.error('Error retrieving security logs summary:', error); + res.status(500).json({ + success: false, + message: 'Failed to retrieve security logs summary' + }); + } +}); + +module.exports = router; \ No newline at end of file