diff --git a/web/cypress/e2e/security.cy.js b/web/cypress/e2e/security.cy.js new file mode 100644 index 000000000000..bfe2f68d0f92 --- /dev/null +++ b/web/cypress/e2e/security.cy.js @@ -0,0 +1,335 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +describe('Security Center E2E Tests', () => { + beforeEach(() => { + // Mock API responses + cy.intercept('GET', '/api/security/dashboard', { + body: { + success: true, + data: { + total_count: 100, + unique_users: 50, + today_count: 10, + active_devices: 25, + unique_ips: 30, + active_anomalies: 5, + anomaly_trend: [ + { time: '2024-01-01', type: 'malicious', count: 5 }, + { time: '2024-01-02', type: 'suspicious', count: 3 }, + ], + device_clusters: [ + { device_type: 'mobile', count: 40 }, + { device_type: 'desktop', count: 35 }, + { device_type: 'tablet', count: 25 }, + ], + ip_analytics: [ + { ip: '192.168.1.1', request_count: 100 }, + { ip: '10.0.0.1', request_count: 80 }, + ], + top_keywords: [ + { keyword: 'test', count: 15 }, + { keyword: 'admin', count: 10 }, + ], + }, + }, + }).as('dashboardData'); + + cy.intercept('GET', '/api/security/devices', { + body: { + success: true, + data: { + devices: [ + { + device_id: 'device1', + device_type: 'mobile', + user_agent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)', + user_count: 5, + request_count: 100, + anomaly_score: 0.3, + is_blocked: false, + is_nat: false, + }, + { + device_id: 'device2', + device_type: 'desktop', + user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', + user_count: 3, + request_count: 50, + anomaly_score: 0.7, + is_blocked: true, + is_nat: true, + }, + ], + total: 2, + }, + }, + }).as('devicesData'); + + cy.intercept('GET', '/api/security/anomalies', { + body: { + success: true, + data: { + anomalies: [ + { + id: 1, + anomaly_type: 'suspicious', + user_id: 'user1', + target_identifier: 'device1', + severity: 'medium', + description: 'Unusual login pattern detected', + status: 'pending', + detected_at: '2024-01-01T12:00:00Z', + }, + { + id: 2, + anomaly_type: 'malicious', + user_id: 'user2', + target_identifier: '192.168.1.1', + severity: 'high', + description: 'Multiple failed login attempts', + status: 'investigating', + detected_at: '2024-01-02T14:30:00Z', + }, + ], + total: 2, + }, + }, + }).as('anomaliesData'); + + cy.intercept('POST', '/api/security/anomalies/1/action', { + body: { success: true }, + }).as('banAnomaly'); + + cy.intercept('POST', '/api/security/devices/device1/action', { + body: { success: true }, + }).as('blockDevice'); + + // Visit security page + cy.visit('/security'); + cy.wait('@dashboardData'); + }); + + it('should display enhanced dashboard with new metrics', () => { + // Check enhanced metrics cards + cy.get('[data-testid="total-violations"]').should('contain', '100'); + cy.get('[data-testid="unique-users"]').should('contain', '50'); + cy.get('[data-testid="today-violations"]').should('contain', '10'); + cy.get('[data-testid="active-devices"]').should('contain', '25'); + cy.get('[data-testid="unique-ips"]').should('contain', '30'); + cy.get('[data-testid="active-anomalies"]').should('contain', '5'); + + // Check charts are rendered + cy.get('[data-testid="anomaly-trend-chart"]').should('be.visible'); + cy.get('[data-testid="device-cluster-chart"]').should('be.visible'); + cy.get('[data-testid="ip-analytics-chart"]').should('be.visible'); + + // Check response actions summary + cy.get('[data-testid="response-actions-summary"]').should('be.visible'); + }); + + it('should navigate between tabs', () => { + // Click on Device Clusters tab + cy.contains('security.deviceClusters').click(); + cy.wait('@devicesData'); + cy.url().should('include', 'devices'); + + // Click on IP Analytics tab + cy.contains('security.ipAnalytics').click(); + cy.url().should('include', 'ips'); + + // Click on Anomaly Management tab + cy.contains('security.anomalyManagement').click(); + cy.wait('@anomaliesData'); + cy.url().should('include', 'anomalies'); + + // Click back to Dashboard + cy.contains('security.dashboard').click(); + cy.wait('@dashboardData'); + cy.url().should('include', 'dashboard'); + }); + + it('should display device clusters with filtering', () => { + cy.contains('security.deviceClusters').click(); + cy.wait('@devicesData'); + + // Check device table + cy.get('[data-testid="devices-table"]').should('be.visible'); + cy.contains('device1').should('be.visible'); + cy.contains('device2').should('be.visible'); + cy.contains('mobile').should('be.visible'); + cy.contains('desktop').should('be.visible'); + + // Test filtering + cy.get('[data-testid="device-type-filter"]').type('mobile'); + cy.get('[data-testid="apply-filter"]').click(); + + // Should only show mobile devices + cy.contains('device1').should('be.visible'); + cy.contains('device2').should('not.exist'); + }); + + it('should handle anomaly management actions', () => { + cy.contains('security.anomalyManagement').click(); + cy.wait('@anomaliesData'); + + // Check anomalies table + cy.get('[data-testid="anomalies-table"]').should('be.visible'); + cy.contains('suspicious').should('be.visible'); + cy.contains('malicious').should('be.visible'); + cy.contains('user1').should('be.visible'); + cy.contains('Unusual login pattern detected').should('be.visible'); + + // Test ban action + cy.contains('security.ban').first().click(); + cy.get('[data-testid="confirm-modal"]').should('be.visible'); + cy.get('[data-testid="confirm-button"]').click(); + cy.wait('@banAnomaly'); + + // Should show success message + cy.get('[data-testid="success-toast"]').should('be.visible'); + }); + + it('should display device details modal', () => { + cy.contains('security.deviceClusters').click(); + cy.wait('@devicesData'); + + // Click view details + cy.contains('security.viewDetails').first().click(); + + // Check modal content + cy.get('[data-testid="device-details-modal"]').should('be.visible'); + cy.contains('security.deviceDetails').should('be.visible'); + cy.contains('device1').should('be.visible'); + cy.contains('mobile').should('be.visible'); + cy.contains('Mozilla/5.0').should('be.visible'); + + // Check associated users table + cy.get('[data-testid="associated-users-table"]').should('be.visible'); + + // Close modal + cy.get('[data-testid="close-modal"]').click(); + cy.get('[data-testid="device-details-modal"]').should('not.exist'); + }); + + it('should handle settings configuration', () => { + cy.contains('security.settings').click(); + + // Check settings sections + cy.contains('security.enforcementSettings').should('be.visible'); + cy.contains('security.detectionSettings').should('be.visible'); + cy.contains('security.notificationSettings').should('be.visible'); + + // Fill enforcement settings + cy.get('[data-testid="violation-redirect-model"]') + .clear() + .type('gpt-4-turbo'); + + cy.get('[data-testid="auto-ban-enabled"]').click(); + + cy.get('[data-testid="auto-ban-threshold"]') + .clear() + .type('15'); + + // Fill detection settings + cy.get('[data-testid="anomaly-detection-enabled"]').click(); + + cy.get('[data-testid="anomaly-threshold-score"]') + .clear() + .type('0.8'); + + // Fill notification settings + cy.get('[data-testid="real-time-alerts-enabled"]').click(); + + cy.get('[data-testid="alert-severity-threshold"]').select('high'); + + // Save settings + cy.get('[data-testid="save-settings"]').click(); + + // Should show success message + cy.get('[data-testid="success-toast"]').should('be.visible'); + }); + + it('should validate form inputs', () => { + cy.contains('security.settings').click(); + + // Test invalid model format + cy.get('[data-testid="violation-redirect-model"]') + .clear() + .type('invalid model@#$'); + + cy.get('[data-testid="save-settings"]').click(); + + // Should show validation error + cy.get('[data-testid="validation-error"]').should('be.visible'); + cy.contains('security.invalidModelFormat').should('be.visible'); + + // Test threshold validation + cy.get('[data-testid="auto-ban-threshold"]') + .clear() + .type('2000'); // Over max limit + + cy.get('[data-testid="save-settings"]').click(); + + cy.contains('security.thresholdRange').should('be.visible'); + }); + + it('should handle responsive design', () => { + // Test mobile view + cy.viewport(375, 667); // iPhone dimensions + cy.visit('/security'); + cy.wait('@dashboardData'); + + // Should stack metrics vertically + cy.get('[data-testid="metrics-row"]').should('have.class', 'responsive-stack'); + + // Charts should be full width + cy.get('[data-testid="chart-container"]').should('have.css', 'width', '100%'); + + // Test tablet view + cy.viewport(768, 1024); // iPad dimensions + cy.get('[data-testid="metrics-row"]').should('not.have.class', 'responsive-stack'); + + // Test desktop view + cy.viewport(1200, 800); // Desktop dimensions + cy.get('[data-testid="tabs-container"]').should('be.visible'); + }); + + it('should handle keyboard navigation and accessibility', () => { + // Test tab navigation + cy.get('body').tab(); + cy.focused().should('contain', 'security.dashboard'); + + // Navigate through tabs using arrow keys + cy.get('body').type('{rightArrow}'); + cy.focused().should('contain', 'security.deviceClusters'); + + // Test modal focus management + cy.contains('security.deviceClusters').click(); + cy.wait('@devicesData'); + cy.contains('security.viewDetails').first().click(); + + // Focus should be trapped in modal + cy.focused().should('be.within', '[data-testid="device-details-modal"]'); + + // Test escape key to close modal + cy.get('body').type('{esc}'); + cy.get('[data-testid="device-details-modal"]').should('not.exist'); + }); +}); \ No newline at end of file diff --git a/web/src/__tests__/Security.test.jsx b/web/src/__tests__/Security.test.jsx new file mode 100644 index 000000000000..29d0af45cbfd --- /dev/null +++ b/web/src/__tests__/Security.test.jsx @@ -0,0 +1,311 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { BrowserRouter } from 'react-router-dom'; +import { I18nextProvider } from 'react-i18next'; +import SecurityCenter from '../pages/Security'; +import { API } from '../helpers'; + +// Mock API calls +jest.mock('../helpers', () => ({ + API: { + get: jest.fn(), + post: jest.fn(), + put: jest.fn(), + delete: jest.fn(), + }, +})); + +// Mock i18n +const i18n = { + t: (key) => key, + changeLanguage: () => {}, +}; + +describe('SecurityCenter', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const renderWithProviders = (component) => { + return render( + + + {component} + + + ); + }; + + test('renders security center with tabs', async () => { + // Mock dashboard API call + API.get.mockResolvedValue({ + data: { + success: true, + data: { + total_count: 100, + unique_users: 50, + today_count: 10, + active_devices: 25, + unique_ips: 30, + active_anomalies: 5, + anomaly_trend: [], + device_clusters: [], + ip_analytics: [], + top_keywords: [], + }, + }, + }); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText('security.title')).toBeInTheDocument(); + expect(screen.getByText('security.dashboard')).toBeInTheDocument(); + expect(screen.getByText('security.deviceClusters')).toBeInTheDocument(); + expect(screen.getByText('security.ipAnalytics')).toBeInTheDocument(); + expect(screen.getByText('security.anomalyManagement')).toBeInTheDocument(); + expect(screen.getByText('security.violations')).toBeInTheDocument(); + expect(screen.getByText('security.users')).toBeInTheDocument(); + expect(screen.getByText('security.settings')).toBeInTheDocument(); + }); + }); + + test('loads dashboard stats on mount', async () => { + const mockData = { + total_count: 100, + unique_users: 50, + today_count: 10, + active_devices: 25, + unique_ips: 30, + active_anomalies: 5, + }; + + API.get.mockResolvedValue({ + data: { success: true, data: mockData }, + }); + + renderWithProviders(); + + await waitFor(() => { + expect(API.get).toHaveBeenCalledWith('/api/security/dashboard'); + expect(screen.getByText('100')).toBeInTheDocument(); // total violations + expect(screen.getByText('50')).toBeInTheDocument(); // unique users + expect(screen.getByText('10')).toBeInTheDocument(); // today violations + expect(screen.getByText('25')).toBeInTheDocument(); // active devices + expect(screen.getByText('30')).toBeInTheDocument(); // unique ips + expect(screen.getByText('5')).toBeInTheDocument(); // active anomalies + }); + }); + + test('handles tab navigation', async () => { + API.get.mockResolvedValue({ + data: { success: true, data: { devices: [], total: 0 } }, + }); + + renderWithProviders(); + + // Click on devices tab + const devicesTab = screen.getByText('security.deviceClusters'); + fireEvent.click(devicesTab); + + await waitFor(() => { + expect(API.get).toHaveBeenCalledWith('/api/security/devices', { + params: { page: 1, page_size: 10 }, + }); + }); + }); + + test('displays device management interface', async () => { + const mockDevices = [ + { + device_id: 'device1', + device_type: 'mobile', + user_agent: 'Mozilla/5.0...', + user_count: 5, + request_count: 100, + anomaly_score: 0.3, + is_blocked: false, + is_nat: false, + }, + ]; + + API.get.mockResolvedValue({ + data: { success: true, data: { devices: mockDevices, total: 1 } }, + }); + + renderWithProviders(); + + // Navigate to devices tab + const devicesTab = screen.getByText('security.deviceClusters'); + fireEvent.click(devicesTab); + + await waitFor(() => { + expect(screen.getByText('device1')).toBeInTheDocument(); + expect(screen.getByText('mobile')).toBeInTheDocument(); + expect(screen.getByText('5')).toBeInTheDocument(); // user count + expect(screen.getByText('100')).toBeInTheDocument(); // request count + }); + }); + + test('handles anomaly actions', async () => { + const mockAnomalies = [ + { + id: 1, + anomaly_type: 'suspicious', + user_id: 'user1', + target_identifier: 'device1', + severity: 'medium', + description: 'Unusual activity detected', + status: 'pending', + detected_at: '2024-01-01T12:00:00Z', + }, + ]; + + API.get.mockResolvedValue({ + data: { success: true, data: { anomalies: mockAnomalies, total: 1 } }, + }); + + API.post.mockResolvedValue({ + data: { success: true }, + }); + + renderWithProviders(); + + // Navigate to anomalies tab + const anomaliesTab = screen.getByText('security.anomalyManagement'); + fireEvent.click(anomalies); + + await waitFor(() => { + expect(screen.getByText('suspicious')).toBeInTheDocument(); + expect(screen.getByText('user1')).toBeInTheDocument(); + expect(screen.getByText('device1')).toBeInTheDocument(); + expect(screen.getByText('Unusual activity detected')).toBeInTheDocument(); + }); + + // Test ban action + const banButton = screen.getByText('security.ban'); + fireEvent.click(banButton); + + await waitFor(() => { + expect(API.post).toHaveBeenCalledWith('/api/security/anomalies/1/action', { + action: 'ban', + }); + }); + }); + + test('handles settings form submission', async () => { + API.get.mockResolvedValue({ + data: { success: true, data: {} }, + }); + + API.put.mockResolvedValue({ + data: { success: true }, + }); + + renderWithProviders(); + + // Navigate to settings tab + const settingsTab = screen.getByText('security.settings'); + fireEvent.click(settingsTab); + + await waitFor(() => { + expect(screen.getByText('security.enforcementSettings')).toBeInTheDocument(); + expect(screen.getByText('security.detectionSettings')).toBeInTheDocument(); + expect(screen.getByText('security.notificationSettings')).toBeInTheDocument(); + }); + + // Fill form and submit + const modelInput = screen.getByPlaceholderText('gpt-3.5-turbo'); + fireEvent.change(modelInput, { target: { value: 'gpt-4' } }); + + const thresholdInput = screen.getByPlaceholderText('10'); + fireEvent.change(thresholdInput, { target: { value: 20 } }); + + const saveButton = screen.getByText('common.save'); + fireEvent.click(saveButton); + + await waitFor(() => { + expect(API.put).toHaveBeenCalledWith('/api/security/settings', { + violation_redirect_model: 'gpt-4', + auto_ban_threshold: 20, + }); + }); + }); + + test('displays error messages on API failure', async () => { + // Mock console.error to avoid test output noise + const originalError = console.error; + console.error = jest.fn(); + + API.get.mockRejectedValue(new Error('Network error')); + + renderWithProviders(); + + await waitFor(() => { + // Should handle API errors gracefully + expect(console.error).toHaveBeenCalled(); + }); + + console.error = originalError; + }); + + test('handles device details modal', async () => { + const mockDevice = { + device_id: 'device1', + device_type: 'mobile', + user_agent: 'Mozilla/5.0...', + user_count: 5, + request_count: 100, + anomaly_score: 0.3, + is_blocked: false, + is_nat: false, + associated_users: [ + { user_id: 'user1', username: 'testuser', last_seen: '2024-01-01T12:00:00Z', request_count: 50 }, + ], + }; + + API.get.mockResolvedValue({ + data: { success: true, data: { devices: [mockDevice], total: 1 } }, + }); + + renderWithProviders(); + + // Navigate to devices tab + const devicesTab = screen.getByText('security.deviceClusters'); + fireEvent.click(devicesTab); + + await waitFor(() => { + expect(screen.getByText('security.viewDetails')).toBeInTheDocument(); + }); + + // Click view details + const viewDetailsButton = screen.getByText('security.viewDetails'); + fireEvent.click(viewDetailsButton); + + await waitFor(() => { + expect(screen.getByText('security.deviceDetails')).toBeInTheDocument(); + expect(screen.getByText('device1')).toBeInTheDocument(); + expect(screen.getByText('mobile')).toBeInTheDocument(); + expect(screen.getByText('testuser')).toBeInTheDocument(); + }); + }); +}); \ No newline at end of file diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index a660b5200b33..e56447ada814 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -60,6 +60,13 @@ "Client ID": "Client ID", "Client Secret": "Client Secret", "common.changeLanguage": "Change Language", + "common.save": "Save", + "common.reset": "Reset", + "common.search": "Search", + "common.refresh": "Refresh", + "common.filter": "Filter", + "common.all": "All", + "common.close": "Close", "default为默认设置,可单独设置每个分类的安全等级": "\"default\" is the default setting, and each category can be set separately", "default为默认设置,可单独设置每个模型的版本": "\"default\" is the default setting, and each model can be set separately", "Dify渠道只适配chatflow和agent,并且agent不支持图片!": "Dify channel only supports chatflow and agent, and agent does not support images!", @@ -2351,6 +2358,103 @@ "security.loadUsersFailed": "Failed to load users", "security.loadSettingsFailed": "Failed to load settings", "security.searchByUserId": "Search by User ID", - "security.searchByKeyword": "Search by Keyword" + "security.searchByKeyword": "Search by Keyword", + "security.deviceClusters": "Device Clusters", + "security.ipAnalytics": "IP Analytics", + "security.anomalyManagement": "Anomaly Management", + "security.activeDevices": "Active Devices", + "security.uniqueIps": "Unique IPs", + "security.activeAnomalies": "Active Anomalies", + "security.last30Days": "Last 30 days", + "security.last24Hours": "Last 24 hours", + "security.uniqueUsers": "Unique users", + "security.uniqueDevices": "Unique devices", + "security.pendingInvestigation": "Pending investigation", + "security.anomalyTrend": "Anomaly Trend", + "security.topIps": "Top IP Addresses", + "security.responseActionsSummary": "Response Actions Summary", + "security.usersBanned": "Users Banned", + "security.usersRedirected": "Users Redirected", + "security.anomaliesResolved": "Anomalies Resolved", + "security.deviceId": "Device ID", + "security.deviceType": "Device Type", + "security.userAgent": "User Agent", + "security.associatedUsers": "Associated Users", + "security.requestCount": "Request Count", + "security.anomalyScore": "Anomaly Score", + "security.blocked": "Blocked", + "security.natDetected": "NAT Detected", + "security.proxyDetected": "Proxy Detected", + "security.viewDetails": "View Details", + "security.block": "Block", + "security.unblock": "Unblock", + "security.confirmBlockDevice": "Are you sure you want to block this device?", + "security.confirmBlockIp": "Are you sure you want to block this IP address?", + "security.ipAddress": "IP Address", + "security.country": "Country", + "security.searchByDeviceType": "Search by device type", + "security.searchByIp": "Search by IP address", + "security.filterByStatus": "Filter by status", + "security.filterByType": "Filter by type", + "security.filterBySeverity": "Filter by severity", + "security.malicious": "Malicious", + "security.suspicious": "Suspicious", + "security.unusual": "Unusual", + "security.high": "High", + "security.medium": "Medium", + "security.low": "Low", + "security.pending": "Pending", + "security.investigating": "Investigating", + "security.resolved": "Resolved", + "security.ignored": "Ignored", + "security.anomalyTime": "Anomaly Time", + "security.anomalyType": "Anomaly Type", + "security.deviceOrIp": "Device/IP", + "security.description": "Description", + "security.ban": "Ban", + "security.redirect": "Redirect", + "security.ignore": "Ignore", + "security.resolve": "Resolve", + "security.deviceDetails": "Device Details", + "security.ipDetails": "IP Details", + "security.lastSeen": "Last Seen", + "security.enforcementSettings": "Enforcement Settings", + "security.detectionSettings": "Detection Settings", + "security.notificationSettings": "Notification Settings", + "security.autoBanDuration": "Auto-ban Duration", + "security.hours": "hours", + "security.anomalyDetectionEnabled": "Enable Anomaly Detection", + "security.anomalyThresholdScore": "Anomaly Threshold Score", + "security.deviceFingerprintingEnabled": "Enable Device Fingerprinting", + "security.ipReputationCheckEnabled": "Enable IP Reputation Check", + "security.maxRequestsPerMinute": "Max Requests per Minute", + "security.maxRequestsPerHour": "Max Requests per Hour", + "security.realTimeAlertsEnabled": "Enable Real-time Alerts", + "security.alertSeverityThreshold": "Alert Severity Threshold", + "security.notificationWebhookUrl": "Notification Webhook URL", + "security.pleaseEnterModel": "Please enter a model name", + "security.invalidModelFormat": "Invalid model format", + "security.pleaseEnterThreshold": "Please enter a threshold value", + "security.thresholdRange": "Threshold must be between 1 and 1000", + "security.durationRange": "Duration must be between 1 and 8760 hours", + "security.scoreRange": "Score must be between 0.1 and 1.0", + "security.requestsRange": "Requests must be between 1 and 10000", + "security.requestsRangeHour": "Requests must be between 1 and 100000", + "security.invalidWebhookUrl": "Invalid webhook URL format", + "security.loadDevicesFailed": "Failed to load devices", + "security.loadIpsFailed": "Failed to load IP addresses", + "security.loadAnomaliesFailed": "Failed to load anomalies", + "security.banSuccess": "Banned successfully", + "security.banFailed": "Ban failed", + "security.redirectSuccess": "Redirected successfully", + "security.redirectFailed": "Redirect failed", + "security.ignoreSuccess": "Ignored successfully", + "security.ignoreFailed": "Ignore failed", + "security.resolveSuccess": "Resolved successfully", + "security.resolveFailed": "Resolve failed", + "security.blockSuccess": "Blocked successfully", + "security.blockFailed": "Block failed", + "security.unblockSuccess": "Unblocked successfully", + "security.unblockFailed": "Unblock failed" } } diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 389c270e272a..6bad6fc8b66f 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -57,7 +57,14 @@ "Claude请求头覆盖": "Claude请求头覆盖", "Client ID": "Client ID", "Client Secret": "Client Secret", - "common.changeLanguage": "common.changeLanguage", + "common.changeLanguage": "切换语言", + "common.save": "保存", + "common.reset": "重置", + "common.search": "搜索", + "common.refresh": "刷新", + "common.filter": "筛选", + "common.all": "全部", + "common.close": "关闭", "default为默认设置,可单独设置每个分类的安全等级": "default为默认设置,可单独设置每个分类的安全等级", "default为默认设置,可单独设置每个模型的版本": "default为默认设置,可单独设置每个模型的版本", "Dify渠道只适配chatflow和agent,并且agent不支持图片!": "Dify渠道只适配chatflow和agent,并且agent不支持图片!", @@ -2267,6 +2274,103 @@ "security.loadSettingsFailed": "加载设置失败", "security.searchByUserId": "按用户ID搜索", "security.searchByKeyword": "按关键词搜索", + "security.deviceClusters": "设备集群", + "security.ipAnalytics": "IP分析", + "security.anomalyManagement": "异常管理", + "security.activeDevices": "活跃设备", + "security.uniqueIps": "唯一IP", + "security.activeAnomalies": "活跃异常", + "security.last30Days": "最近30天", + "security.last24Hours": "最近24小时", + "security.uniqueUsers": "唯一用户", + "security.uniqueDevices": "唯一设备", + "security.pendingInvestigation": "待调查", + "security.anomalyTrend": "异常趋势", + "security.topIps": "热门IP地址", + "security.responseActionsSummary": "响应操作汇总", + "security.usersBanned": "用户已封禁", + "security.usersRedirected": "用户已重定向", + "security.anomaliesResolved": "异常已解决", + "security.deviceId": "设备ID", + "security.deviceType": "设备类型", + "security.userAgent": "用户代理", + "security.associatedUsers": "关联用户", + "security.requestCount": "请求次数", + "security.anomalyScore": "异常评分", + "security.blocked": "已阻止", + "security.natDetected": "检测到NAT", + "security.proxyDetected": "检测到代理", + "security.viewDetails": "查看详情", + "security.block": "阻止", + "security.unblock": "解除阻止", + "security.confirmBlockDevice": "确定要阻止此设备吗?", + "security.confirmBlockIp": "确定要阻止此IP地址吗?", + "security.ipAddress": "IP地址", + "security.country": "国家", + "security.searchByDeviceType": "按设备类型搜索", + "security.searchByIp": "按IP地址搜索", + "security.filterByStatus": "按状态筛选", + "security.filterByType": "按类型筛选", + "security.filterBySeverity": "按严重程度筛选", + "security.malicious": "恶意", + "security.suspicious": "可疑", + "security.unusual": "异常", + "security.high": "高", + "security.medium": "中", + "security.low": "低", + "security.pending": "待处理", + "security.investigating": "调查中", + "security.resolved": "已解决", + "security.ignored": "已忽略", + "security.anomalyTime": "异常时间", + "security.anomalyType": "异常类型", + "security.deviceOrIp": "设备/IP", + "security.description": "描述", + "security.ban": "封禁", + "security.redirect": "重定向", + "security.ignore": "忽略", + "security.resolve": "解决", + "security.deviceDetails": "设备详情", + "security.ipDetails": "IP详情", + "security.lastSeen": "最后访问", + "security.enforcementSettings": "执行设置", + "security.detectionSettings": "检测设置", + "security.notificationSettings": "通知设置", + "security.autoBanDuration": "自动封禁时长", + "security.hours": "小时", + "security.anomalyDetectionEnabled": "启用异常检测", + "security.anomalyThresholdScore": "异常阈值分数", + "security.deviceFingerprintingEnabled": "启用设备指纹识别", + "security.ipReputationCheckEnabled": "启用IP声誉检查", + "security.maxRequestsPerMinute": "每分钟最大请求数", + "security.maxRequestsPerHour": "每小时最大请求数", + "security.realTimeAlertsEnabled": "启用实时警报", + "security.alertSeverityThreshold": "警报严重程度阈值", + "security.notificationWebhookUrl": "通知Webhook URL", + "security.pleaseEnterModel": "请输入模型名称", + "security.invalidModelFormat": "模型格式无效", + "security.pleaseEnterThreshold": "请输入阈值", + "security.thresholdRange": "阈值必须在1到1000之间", + "security.durationRange": "时长必须在1到8760小时之间", + "security.scoreRange": "分数必须在0.1到1.0之间", + "security.requestsRange": "请求数必须在1到10000之间", + "security.requestsRangeHour": "请求数必须在1到100000之间", + "security.invalidWebhookUrl": "Webhook URL格式无效", + "security.loadDevicesFailed": "加载设备失败", + "security.loadIpsFailed": "加载IP地址失败", + "security.loadAnomaliesFailed": "加载异常失败", + "security.banSuccess": "封禁成功", + "security.banFailed": "封禁失败", + "security.redirectSuccess": "重定向成功", + "security.redirectFailed": "重定向失败", + "security.ignoreSuccess": "忽略成功", + "security.ignoreFailed": "忽略失败", + "security.resolveSuccess": "解决成功", + "security.resolveFailed": "解决失败", + "security.blockSuccess": "阻止成功", + "security.blockFailed": "阻止失败", + "security.unblockSuccess": "解除阻止成功", + "security.unblockFailed": "解除阻止失败", "工单系统": "工单系统", "创建工单": "创建工单", "工单列表": "工单列表", diff --git a/web/src/pages/Security/index.jsx b/web/src/pages/Security/index.jsx index c10b7b16501d..ff8149a62b8d 100644 --- a/web/src/pages/Security/index.jsx +++ b/web/src/pages/Security/index.jsx @@ -17,7 +17,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Card, @@ -37,6 +37,14 @@ import { Row, Col, Spin, + Divider, + Switch, + InputNumber, + Popconfirm, + Timeline, + Badge, + Progress, + TextArea, } from '@douyinfe/semi-ui'; import { IconShieldStroked, @@ -45,7 +53,16 @@ import { IconDelete, IconSearch, IconRefresh, + IconMonitor, + IconGlobe, + IconActivity, + IconBan, + IconCheckCircleStroked, + IconExclamationTriangle, + IconEyeOpened, + IconFilter, } from '@douyinfe/semi-icons'; +import { VChart } from '@visactor/react-vchart'; import { API } from '../../helpers'; const { Title, Text } = Typography; @@ -59,6 +76,32 @@ const SecurityCenter = () => { const [stats, setStats] = useState({}); const [statsLoading, setStatsLoading] = useState(false); + // Charts data + const [anomalyTrendData, setAnomalyTrendData] = useState([]); + const [deviceClusterData, setDeviceClusterData] = useState([]); + const [ipAnalyticsData, setIpAnalyticsData] = useState([]); + + // Device clusters + const [devices, setDevices] = useState([]); + const [devicesTotal, setDevicesTotal] = useState(0); + const [devicesPage, setDevicesPage] = useState(1); + const [devicesPageSize] = useState(10); + const [devicesFilters, setDevicesFilters] = useState({}); + + // IP analytics + const [ips, setIps] = useState([]); + const [ipsTotal, setIpsTotal] = useState(0); + const [ipsPage, setIpsPage] = useState(1); + const [ipsPageSize] = useState(10); + const [ipsFilters, setIpsFilters] = useState({}); + + // Anomalies + const [anomalies, setAnomalies] = useState([]); + const [anomaliesTotal, setAnomaliesTotal] = useState(0); + const [anomaliesPage, setAnomaliesPage] = useState(1); + const [anomaliesPageSize] = useState(10); + const [anomaliesFilters, setAnomaliesFilters] = useState({}); + // Violations const [violations, setViolations] = useState([]); const [violationsTotal, setViolationsTotal] = useState(0); @@ -80,10 +123,20 @@ const SecurityCenter = () => { const [redirectModalVisible, setRedirectModalVisible] = useState(false); const [selectedUser, setSelectedUser] = useState(null); const [redirectModel, setRedirectModel] = useState(''); + const [deviceModalVisible, setDeviceModalVisible] = useState(false); + const [selectedDevice, setSelectedDevice] = useState(null); + const [ipModalVisible, setIpModalVisible] = useState(false); + const [selectedIp, setSelectedIp] = useState(null); useEffect(() => { if (activeTab === 'dashboard') { loadDashboardStats(); + } else if (activeTab === 'devices') { + loadDevices(); + } else if (activeTab === 'ips') { + loadIps(); + } else if (activeTab === 'anomalies') { + loadAnomalies(); } else if (activeTab === 'violations') { loadViolations(); } else if (activeTab === 'users') { @@ -91,7 +144,7 @@ const SecurityCenter = () => { } else if (activeTab === 'settings') { loadSettings(); } - }, [activeTab, violationsPage, usersPage]); + }, [activeTab, violationsPage, usersPage, devicesPage, ipsPage, anomaliesPage]); const loadDashboardStats = async () => { setStatsLoading(true); @@ -99,6 +152,10 @@ const SecurityCenter = () => { const res = await API.get('/api/security/dashboard'); if (res.data.success) { setStats(res.data.data); + // Set chart data + setAnomalyTrendData(res.data.data.anomaly_trend || []); + setDeviceClusterData(res.data.data.device_clusters || []); + setIpAnalyticsData(res.data.data.ip_analytics || []); } } catch (error) { Toast.error(t('security.loadStatsFailed')); @@ -127,6 +184,66 @@ const SecurityCenter = () => { } }; + const loadDevices = async () => { + setLoading(true); + try { + const params = { + page: devicesPage, + page_size: devicesPageSize, + ...devicesFilters, + }; + const res = await API.get('/api/security/devices', { params }); + if (res.data.success) { + setDevices(res.data.data.devices || []); + setDevicesTotal(res.data.data.total || 0); + } + } catch (error) { + Toast.error(t('security.loadDevicesFailed')); + } finally { + setLoading(false); + } + }; + + const loadIps = async () => { + setLoading(true); + try { + const params = { + page: ipsPage, + page_size: ipsPageSize, + ...ipsFilters, + }; + const res = await API.get('/api/security/ips', { params }); + if (res.data.success) { + setIps(res.data.data.ips || []); + setIpsTotal(res.data.data.total || 0); + } + } catch (error) { + Toast.error(t('security.loadIpsFailed')); + } finally { + setLoading(false); + } + }; + + const loadAnomalies = async () => { + setLoading(true); + try { + const params = { + page: anomaliesPage, + page_size: anomaliesPageSize, + ...anomaliesFilters, + }; + const res = await API.get('/api/security/anomalies', { params }); + if (res.data.success) { + setAnomalies(res.data.data.anomalies || []); + setAnomaliesTotal(res.data.data.total || 0); + } + } catch (error) { + Toast.error(t('security.loadAnomaliesFailed')); + } finally { + setLoading(false); + } + }; + const loadUsers = async () => { setLoading(true); try { @@ -258,6 +375,142 @@ const SecurityCenter = () => { } }; + const handleAnomalyAction = async (anomalyId, action) => { + try { + const res = await API.post(`/api/security/anomalies/${anomalyId}/action`, { action }); + if (res.data.success) { + Toast.success(t(`security.${action}Success`)); + loadAnomalies(); + loadDashboardStats(); // Refresh dashboard stats + } + } catch (error) { + Toast.error(t(`security.${action}Failed`)); + } + }; + + const handleDeviceAction = async (deviceId, action) => { + try { + const res = await API.post(`/api/security/devices/${deviceId}/action`, { action }); + if (res.data.success) { + Toast.success(t(`security.${action}Success`)); + loadDevices(); + loadDashboardStats(); + } + } catch (error) { + Toast.error(t(`security.${action}Failed`)); + } + }; + + const handleIpAction = async (ipId, action) => { + try { + const res = await API.post(`/api/security/ips/${ipId}/action`, { action }); + if (res.data.success) { + Toast.success(t(`security.${action}Success`)); + loadIps(); + loadDashboardStats(); + } + } catch (error) { + Toast.error(t(`security.${action}Failed`)); + } + }; + + const handleViewDeviceDetails = (device) => { + setSelectedDevice(device); + setDeviceModalVisible(true); + }; + + const handleViewIpDetails = (ip) => { + setSelectedIp(ip); + setIpModalVisible(true); + }; + + // Chart specifications + const anomalyTrendSpec = { + type: 'line', + data: { + values: anomalyTrendData + }, + xField: 'time', + yField: 'count', + seriesField: 'type', + title: { + visible: true, + text: t('security.anomalyTrend') + }, + legends: { + visible: true, + selectMode: 'single' + }, + tooltip: { + mark: { + content: [ + { + key: (datum) => datum['type'], + value: (datum) => datum['count'] + } + ] + } + }, + color: ['#ff6b6b', '#4ecdc4', '#45b7d1'] + }; + + const deviceClusterSpec = { + type: 'pie', + data: { + values: deviceClusterData + }, + outerRadius: 0.8, + innerRadius: 0.5, + valueField: 'count', + categoryField: 'device_type', + title: { + visible: true, + text: t('security.deviceClusters') + }, + legends: { + visible: true, + orient: 'left' + }, + label: { + visible: true + }, + tooltip: { + mark: { + content: [ + { + key: (datum) => datum['device_type'], + value: (datum) => datum['count'] + } + ] + } + }, + color: ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeaa7'] + }; + + const ipAnalyticsSpec = { + type: 'bar', + data: { + values: ipAnalyticsData.slice(0, 20) // Top 20 IPs + }, + xField: 'ip', + yField: 'request_count', + title: { + visible: true, + text: t('security.topIps') + }, + tooltip: { + mark: { + content: [ + { + key: (datum) => datum['ip'], + value: (datum) => datum['request_count'] + } + ] + } + }, + color: '#45b7d1' + }; + const violationColumns = [ { title: t('security.violationTime'), @@ -388,10 +641,274 @@ const SecurityCenter = () => { }, ]; + const deviceColumns = [ + { + title: t('security.deviceId'), + dataIndex: 'device_id', + }, + { + title: t('security.deviceType'), + dataIndex: 'device_type', + render: (text) => {text}, + }, + { + title: t('security.userAgent'), + dataIndex: 'user_agent', + render: (text) => ( + + {text} + + ), + }, + { + title: t('security.associatedUsers'), + dataIndex: 'user_count', + sorter: (a, b) => a.user_count - b.user_count, + }, + { + title: t('security.requestCount'), + dataIndex: 'request_count', + sorter: (a, b) => a.request_count - b.request_count, + }, + { + title: t('security.anomalyScore'), + dataIndex: 'anomaly_score', + render: (score) => ( + 0.7 ? '#ff6b6b' : score > 0.4 ? '#ffa726' : '#66bb6a'} + /> + ), + }, + { + title: t('security.status'), + render: (_, record) => ( + + {record.is_blocked && {t('security.blocked')}} + {record.is_nat && {t('security.natDetected')}} + {!record.is_blocked && !record.is_nat && ( + {t('security.normal')} + )} + + ), + }, + { + title: t('security.operations'), + render: (_, record) => ( + + + {!record.is_blocked ? ( + handleDeviceAction(record.device_id, 'block')} + > + + + ) : ( + + )} + + ), + }, + ]; + + const ipColumns = [ + { + title: t('security.ipAddress'), + dataIndex: 'ip_address', + }, + { + title: t('security.country'), + dataIndex: 'country', + render: (text) => text ? {text} : '-', + }, + { + title: t('security.associatedUsers'), + dataIndex: 'user_count', + sorter: (a, b) => a.user_count - b.user_count, + }, + { + title: t('security.requestCount'), + dataIndex: 'request_count', + sorter: (a, b) => a.request_count - b.request_count, + }, + { + title: t('security.anomalyScore'), + dataIndex: 'anomaly_score', + render: (score) => ( + 0.7 ? '#ff6b6b' : score > 0.4 ? '#ffa726' : '#66bb6a'} + /> + ), + }, + { + title: t('security.status'), + render: (_, record) => ( + + {record.is_blocked && {t('security.blocked')}} + {record.is_nat && {t('security.natDetected')}} + {record.is_proxy && {t('security.proxyDetected')}} + {!record.is_blocked && !record.is_nat && !record.is_proxy && ( + {t('security.normal')} + )} + + ), + }, + { + title: t('security.operations'), + render: (_, record) => ( + + + {!record.is_blocked ? ( + handleIpAction(record.ip_id, 'block')} + > + + + ) : ( + + )} + + ), + }, + ]; + + const anomalyColumns = [ + { + title: t('security.anomalyTime'), + dataIndex: 'detected_at', + render: (text) => new Date(text).toLocaleString(), + }, + { + title: t('security.anomalyType'), + dataIndex: 'anomaly_type', + render: (text) => ( + + {text} + + ), + }, + { + title: t('security.userId'), + dataIndex: 'user_id', + }, + { + title: t('security.deviceOrIp'), + dataIndex: 'target_identifier', + }, + { + title: t('security.severity'), + dataIndex: 'severity', + render: (text) => ( + + {text} + + ), + }, + { + title: t('security.description'), + dataIndex: 'description', + render: (text) => ( + + {text} + + ), + }, + { + title: t('security.status'), + dataIndex: 'status', + render: (status) => ( + + {status} + + ), + }, + { + title: t('security.operations'), + render: (_, record) => ( + + {record.status === 'pending' && ( + <> + handleAnomalyAction(record.id, 'ban')} + > + + + + + + )} + {record.status === 'investigating' && ( + + )} + + ), + }, + ]; + const renderDashboard = () => ( - - + {/* Enhanced Metrics Cards */} + + @@ -401,9 +918,12 @@ const SecurityCenter = () => { } > {stats.total_count || 0} + + {t('security.last30Days')} + - + @@ -413,9 +933,12 @@ const SecurityCenter = () => { } > {stats.unique_users || 0} + + {t('security.uniqueUsers')} + - + @@ -425,24 +948,130 @@ const SecurityCenter = () => { } > {stats.today_count || 0} + + {t('security.last24Hours')} + + + + + + + {t('security.activeDevices')} + + } + > + {stats.active_devices || 0} + + {t('security.uniqueDevices')} + + + + + + + {t('security.uniqueIps')} + + } + > + {stats.unique_ips || 0} + + {t('security.uniqueIps')} + + + + + + + {t('security.activeAnomalies')} + + } + > + {stats.active_anomalies || 0} + + {t('security.pendingInvestigation')} + - - {stats.top_keywords && stats.top_keywords.length > 0 ? ( - - ) : ( - {t('security.noData')} - )} - + {/* Charts Section */} + + + + + + + + + + + + + + + + + + + + + + {/* Response Actions Summary */} + + + + + +
+ + {stats.actions_banned || 0} + + {t('security.usersBanned')} +
+ + +
+ + {stats.actions_redirected || 0} + + {t('security.usersRedirected')} +
+ + +
+ + {stats.actions_resolved || 0} + + {t('security.anomaliesResolved')} +
+ + + + + + + {stats.top_keywords && stats.top_keywords.length > 0 ? ( +
+ ) : ( + {t('security.noData')} + )} + + + ); @@ -497,6 +1126,199 @@ const SecurityCenter = () => { ); + const renderDevices = () => ( +
+ +
+ + + setDevicesFilters({ ...devicesFilters, device_type: value }) + } + /> + + setDevicesFilters({ ...devicesFilters, status: value }) + } + style={{ width: 150 }} + > + {t('common.all')} + {t('security.normal')} + {t('security.blocked')} + {t('security.natDetected')} + + + + + +
+ + +
setDevicesPage(page), + }} + /> + + + ); + + const renderIps = () => ( +
+ +
+ + + setIpsFilters({ ...ipsFilters, ip_address: value }) + } + /> + + setIpsFilters({ ...ipsFilters, status: value }) + } + style={{ width: 150 }} + > + {t('common.all')} + {t('security.normal')} + {t('security.blocked')} + {t('security.natDetected')} + {t('security.proxyDetected')} + + + + + +
+ + +
setIpsPage(page), + }} + /> + + + ); + + const renderAnomalies = () => ( +
+ +
+ + + setAnomaliesFilters({ ...anomaliesFilters, anomaly_type: value }) + } + style={{ width: 150 }} + > + {t('common.all')} + {t('security.malicious')} + {t('security.suspicious')} + {t('security.unusual')} + + + setAnomaliesFilters({ ...anomaliesFilters, severity: value }) + } + style={{ width: 150 }} + > + {t('common.all')} + {t('security.high')} + {t('security.medium')} + {t('security.low')} + + + setAnomaliesFilters({ ...anomaliesFilters, status: value }) + } + style={{ width: 150 }} + > + {t('common.all')} + {t('security.pending')} + {t('security.investigating')} + {t('security.resolved')} + {t('security.ignored')} + + + + + +
+ + +
setAnomaliesPage(page), + }} + /> + + + ); + const renderUsers = () => (
+ + + + {t('security.detectionSettings')} + + + + + + + + + + + + + + {t('security.notificationSettings')} + + + + + {t('security.low')} + {t('security.medium')} + {t('security.high')} + + + + +
+ + +
); @@ -570,6 +1505,15 @@ const SecurityCenter = () => { {renderDashboard()} + + {renderDevices()} + + + {renderIps()} + + + {renderAnomalies()} + {renderViolations()} @@ -581,6 +1525,114 @@ const SecurityCenter = () => { + {/* Device Details Modal */} + setDeviceModalVisible(false)} + footer={[ + + ]} + width={800} + > + {selectedDevice && ( +
+ +
+ {t('security.deviceId')}: {selectedDevice.device_id} + + + {t('security.deviceType')}: + {selectedDevice.device_type} + + + + + {t('security.userAgent')}: + + {selectedDevice.user_agent} + + + + {t('security.anomalyScore')}: + 0.7 ? '#ff6b6b' : selectedDevice.anomaly_score > 0.4 ? '#ffa726' : '#66bb6a'} + /> + + + {t('security.associatedUsers')} +
new Date(text).toLocaleString() }, + { title: t('security.requestCount'), dataIndex: 'request_count' }, + ]} + dataSource={selectedDevice.associated_users || []} + pagination={false} + size="small" + /> + + )} + + + {/* IP Details Modal */} + setIpModalVisible(false)} + footer={[ + + ]} + width={800} + > + {selectedIp && ( +
+ +
+ {t('security.ipAddress')}: {selectedIp.ip_address} + + + {t('security.country')}: + {selectedIp.country || 'Unknown'} + + + + + {t('security.anomalyScore')}: + 0.7 ? '#ff6b6b' : selectedIp.anomaly_score > 0.4 ? '#ffa726' : '#66bb6a'} + /> + + + {t('security.requestCount')}: {selectedIp.request_count} + + + {t('security.associatedUsers')} +
new Date(text).toLocaleString() }, + { title: t('security.requestCount'), dataIndex: 'request_count' }, + ]} + dataSource={selectedIp.associated_users || []} + pagination={false} + size="small" + /> + + )} + + + {/* Redirect Modal */}