diff --git a/src/templates/dashboard_template.py b/src/templates/dashboard_template.py index af8c990..dab9a4a 100644 --- a/src/templates/dashboard_template.py +++ b/src/templates/dashboard_template.py @@ -283,7 +283,7 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str: background: #161b22; border: 1px solid #30363d; border-radius: 6px; - padding: 20px; + padding: 12px; margin-bottom: 20px; }} h2 {{ @@ -752,8 +752,30 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str: .leaflet-bottom.leaflet-right {{ display: none !important; }} + .charts-container {{ + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + margin-top: 20px; + }} + .chart-section {{ + display: flex; + flex-direction: column; + }} + .chart-wrapper {{ + display: flex; + flex-direction: column; + }} #attack-types-chart {{ - max-height: 400px; + max-height: 350px; + }} + #attack-patterns-chart {{ + max-height: 350px; + }} + @media (max-width: 1200px) {{ + .charts-container {{ + grid-template-columns: 1fr; + }} }} /* Raw Request Modal */ @@ -841,6 +863,132 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str: background: #2ea043; }} + /* Attack Types Cell Styling */ + .attack-types-cell {{ + max-width: 280px; + position: relative; + display: inline-block; + width: 100%; + overflow: visible; + }} + .attack-types-truncated {{ + display: block; + width: 100%; + max-width: 280px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: #fb8500; + font-weight: 500; + transition: all 0.2s; + position: relative; + }} + .attack-types-tooltip {{ + position: absolute; + bottom: 100%; + left: 0; + background: #0d1117; + border: 1px solid #30363d; + border-radius: 6px; + padding: 12px; + margin-bottom: 8px; + max-width: 400px; + word-wrap: break-word; + white-space: normal; + z-index: 1000; + color: #c9d1d9; + font-size: 12px; + font-weight: normal; + display: none; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); + pointer-events: auto; + }} + .attack-types-cell:hover .attack-types-tooltip {{ + display: block; + }} + .attack-types-tooltip::after {{ + content: ''; + position: absolute; + top: 100%; + left: 12px; + border: 6px solid transparent; + border-top-color: #30363d; + }} + .attack-types-tooltip::before {{ + content: ''; + position: absolute; + top: 100%; + left: 13px; + border: 5px solid transparent; + border-top-color: #0d1117; + z-index: 1; + }} + + /* Path Cell Styling for Attack Table */ + .path-cell-container {{ + position: relative; + display: inline-block; + max-width: 100%; + }} + .path-truncated {{ + display: block; + max-width: 250px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: pointer; + color: #f85149 !important; + font-weight: 500; + text-decoration: underline; + text-decoration-style: dotted; + text-underline-offset: 3px; + transition: all 0.2s; + }} + .path-truncated:hover {{ + color: #ff7369 !important; + text-decoration-style: solid; + }} + .path-cell-container:hover .path-tooltip {{ + display: block; + }} + .path-tooltip {{ + position: absolute; + bottom: 100%; + left: 0; + background: #0d1117; + border: 1px solid #30363d; + border-radius: 6px; + padding: 8px 12px; + margin-bottom: 8px; + max-width: 500px; + word-wrap: break-word; + white-space: normal; + z-index: 1000; + color: #c9d1d9; + font-size: 12px; + font-weight: normal; + display: none; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); + font-family: 'Courier New', monospace; + }} + .path-tooltip::after {{ + content: ''; + position: absolute; + top: 100%; + left: 12px; + border: 6px solid transparent; + border-top-color: #30363d; + }} + .path-tooltip::before {{ + content: ''; + position: absolute; + top: 100%; + left: 13px; + border: 5px solid transparent; + border-top-color: #0d1117; + z-index: 1; + }} + /* Mobile Optimization - Tablets (768px and down) */ @media (max-width: 768px) {{ body {{ @@ -1454,16 +1602,52 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str: -
-
-

Most Recurring Attack Types

-
Top 10 Attack Vectors
+
+
+
+
+

Most Recurring Attack Types

+
Top 10
+
+
+ +
+
-
- + +
+
+
+

Most Recurring Attack Patterns

+
+
+ Page 1/1 + + 0 total +
+ + +
+
+
+ + + + + + + + + + + + + +
#Attack PatternAttack TypeFrequencyIPs
Loading...
+
+
-
@@ -2019,6 +2203,10 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str: loadOverviewTable('credentials'); overviewState.credentials.loaded = true; }} + if (!overviewState.patterns.loaded) {{ + loadOverviewTable('patterns'); + overviewState.patterns.loaded = true; + }} }} }} @@ -2413,7 +2601,8 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str: 'top-ips': {{ currentPage: 1, totalPages: 1, total: 0, sortBy: 'count', sortOrder: 'desc' }}, 'top-paths': {{ currentPage: 1, totalPages: 1, total: 0, sortBy: 'count', sortOrder: 'desc' }}, 'top-ua': {{ currentPage: 1, totalPages: 1, total: 0, sortBy: 'count', sortOrder: 'desc' }}, - attacks: {{ currentPage: 1, totalPages: 1, total: 0, sortBy: 'timestamp', sortOrder: 'desc', loaded: false }} + attacks: {{ currentPage: 1, totalPages: 1, total: 0, sortBy: 'timestamp', sortOrder: 'desc', loaded: false }}, + patterns: {{ currentPage: 1, totalPages: 1, total: 0, sortBy: 'count', sortOrder: 'desc', loaded: false }} }}; const tableConfig = {{ @@ -2422,7 +2611,8 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str: 'top-ips': {{ endpoint: 'top-ips', dataKey: 'ips', cellCount: 3, columns: ['ip', 'count'] }}, 'top-paths': {{ endpoint: 'top-paths', dataKey: 'paths', cellCount: 3, columns: ['path', 'count'] }}, 'top-ua': {{ endpoint: 'top-user-agents', dataKey: 'user_agents', cellCount: 3, columns: ['user_agent', 'count'] }}, - attacks: {{ endpoint: 'attack-types', dataKey: 'attacks', cellCount: 7, columns: ['ip', 'path', 'attack_types', 'user_agent', 'timestamp', 'raw_request'] }} + attacks: {{ endpoint: 'attack-types', dataKey: 'attacks', cellCount: 7, columns: ['ip', 'path', 'attack_types', 'user_agent', 'timestamp', 'raw_request'] }}, + patterns: {{ endpoint: 'attack-patterns', dataKey: 'patterns', cellCount: 5, columns: ['pattern', 'attack_type', 'count', 'ips'] }} }}; // Load overview table on page load @@ -2438,13 +2628,89 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str: tbody.style.opacity = '0'; try {{ - const url = DASHBOARD_PATH + '/api/' + config.endpoint + '?page=' + state.currentPage + '&page_size=5&sort_by=' + state.sortBy + '&sort_order=' + state.sortOrder; - const response = await fetch(url, {{ cache: 'no-store', headers: {{ 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' }} }}); - if (!response.ok) throw new Error(`HTTP ${{response.status}}`); + let items; + let pagination; + + // Special handling for attack patterns + if (tableId === 'patterns') {{ + // Fetch all attacks to extract and aggregate patterns + try {{ + const patternsUrl = DASHBOARD_PATH + '/api/attack-types?page=1&page_size=1000&sort_by=timestamp&sort_order=desc'; + const patternsResponse = await fetch(patternsUrl, {{ cache: 'no-store', headers: {{ 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' }} }}); + if (!patternsResponse.ok) throw new Error('Failed to fetch patterns'); + + const patternsData = await patternsResponse.json(); + const allAttacks = patternsData.attacks || []; + + // Aggregate patterns + const patternMap = {{}}; + allAttacks.forEach(attack => {{ + // Extract patterns from the path or query + const patterns = []; + if (attack.path && attack.path.length < 200) patterns.push(attack.path); + + patterns.forEach(pattern => {{ + if (!patternMap[pattern]) {{ + patternMap[pattern] = {{ + pattern: pattern, + attack_type: (attack.attack_types && attack.attack_types[0]) || 'Unknown', + count: 0, + example_ips: [] + }}; + }} + patternMap[pattern].count++; + if (patternMap[pattern].example_ips.indexOf(attack.ip) === -1) {{ + patternMap[pattern].example_ips.push(attack.ip); + }} + }}); + }}); + + // Convert to sorted array based on current sort preferences + const patternArray = Object.values(patternMap); + + // Apply sorting based on state + patternArray.sort((a, b) => {{ + let aValue, bValue; + + if (state.sortBy === 'count') {{ + aValue = a.count; + bValue = b.count; + return state.sortOrder === 'desc' ? bValue - aValue : aValue - bValue; + }} else if (state.sortBy === 'pattern') {{ + aValue = a.pattern || ''; + bValue = b.pattern || ''; + return state.sortOrder === 'desc' ? bValue.localeCompare(aValue) : aValue.localeCompare(bValue); + }} else if (state.sortBy === 'attack_type') {{ + aValue = a.attack_type || ''; + bValue = b.attack_type || ''; + return state.sortOrder === 'desc' ? bValue.localeCompare(aValue) : aValue.localeCompare(bValue); + }} + // Default to count desc + return b.count - a.count; + }}); + + items = patternArray.slice((state.currentPage - 1) * 5, state.currentPage * 5); + + const totalPatterns = Object.values(patternMap).length; + pagination = {{ + page: state.currentPage, + total_pages: Math.ceil(totalPatterns / 5), + total: totalPatterns + }}; + }} catch (err) {{ + console.error('Error processing patterns:', err); + items = []; + pagination = {{ page: 1, total_pages: 1, total: 0 }}; + }} + }} else {{ + const url = DASHBOARD_PATH + '/api/' + config.endpoint + '?page=' + state.currentPage + '&page_size=5&sort_by=' + state.sortBy + '&sort_order=' + state.sortOrder; + const response = await fetch(url, {{ cache: 'no-store', headers: {{ 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' }} }}); + if (!response.ok) throw new Error(`HTTP ${{response.status}}`); - const data = await response.json(); - const items = data[config.dataKey] || []; - const pagination = data.pagination || {{}}; + const data = await response.json(); + items = data[config.dataKey] || []; + pagination = data.pagination || {{}}; + }} state.currentPage = pagination.page || 1; state.totalPages = pagination.total_pages || 1; @@ -2499,7 +2765,19 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str: const actionBtn = item.raw_request ? `` : `N/A`; - html += `${{rank}}${{item.ip}}${{item.path}}${{item.attack_types.join(', ')}}${{item.user_agent.substring(0, 60)}}${{formatTimestamp(item.timestamp, true)}}${{actionBtn}}`; + const attackTypesStr = item.attack_types.join(', '); + const itemId = item.id || ''; + const attackTypesHtml = `
+
${{attackTypesStr}}
+ ${{attackTypesStr.length > 50 ? `
${{attackTypesStr}}
` : ''}} +
`; + const pathStr = item.path || ''; + const truncatedPath = pathStr.length > 50 ? pathStr.substring(0, 47) + '...' : pathStr; + const pathHtml = `
+
${{truncatedPath}}
+ ${{pathStr.length > 50 ? `
${{pathStr}}
` : ''}} +
`; + html += `${{rank}}${{item.ip}}${{pathHtml}}${{attackTypesHtml}}${{item.user_agent.substring(0, 60)}}${{formatTimestamp(item.timestamp, true)}}${{actionBtn}}`; html += `
@@ -2507,6 +2785,11 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
`; + }} else if (tableId === 'patterns') {{ + const patternStr = item.pattern || '(empty)'; + const exampleIps = item.example_ips ? item.example_ips.slice(0, 3).join(', ') : 'N/A'; + const ipText = item.example_ips && item.example_ips.length > 3 ? exampleIps + '...' : exampleIps; + html += `${{rank}}${{patternStr.substring(0, 60)}}${{item.attack_type || 'Unknown'}}${{item.count}}${{ipText}}`; }} }}); @@ -3079,8 +3362,7 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str: const canvas = document.getElementById('attack-types-chart'); if (!canvas) return; - // Use the new efficient aggregated endpoint - const response = await fetch(DASHBOARD_PATH + '/api/attack-types-stats?limit=20', {{ + const response = await fetch(DASHBOARD_PATH + '/api/attack-types?page=1&page_size=100', {{ cache: 'no-store', headers: {{ 'Cache-Control': 'no-cache', @@ -3088,64 +3370,69 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str: }} }}); - if (!response.ok) throw new Error('Failed to fetch attack types stats'); - + if (!response.ok) throw new Error('Failed to fetch attack types'); + const data = await response.json(); - const attackTypes = data.attack_types || []; + const attacks = data.attacks || []; - if (attackTypes.length === 0) {{ + if (attacks.length === 0) {{ canvas.style.display = 'none'; return; }} - // Data is already aggregated and sorted from the database - const labels = attackTypes.slice(0, 10).map(item => item.type); - const counts = attackTypes.slice(0, 10).map(item => item.count); + // Aggregate attack types + const attackCounts = {{}}; + attacks.forEach(attack => {{ + if (attack.attack_types && Array.isArray(attack.attack_types)) {{ + attack.attack_types.forEach(type => {{ + attackCounts[type] = (attackCounts[type] || 0) + 1; + }}); + }} + }}); + + // Sort and get top 10 + const sortedAttacks = Object.entries(attackCounts) + .sort((a, b) => b[1] - a[1]) + .slice(0, 10); + + if (sortedAttacks.length === 0) {{ + canvas.style.display = 'none'; + return; + }} + + const labels = sortedAttacks.map(([type]) => type); + const counts = sortedAttacks.map(([, count]) => count); const maxCount = Math.max(...counts); - // Enhanced color palette with gradients - const colorMap = {{ - 'SQL Injection': 'rgba(233, 105, 113, 0.85)', - 'XSS': 'rgba(240, 136, 62, 0.85)', - 'Directory Traversal': 'rgba(248, 150, 56, 0.85)', - 'Command Injection': 'rgba(229, 229, 16, 0.85)', - 'Path Traversal': 'rgba(123, 201, 71, 0.85)', - 'Malware': 'rgba(88, 166, 255, 0.85)', - 'Brute Force': 'rgba(79, 161, 246, 0.85)', - 'DDoS': 'rgba(139, 148, 244, 0.85)', - 'CSRF': 'rgba(188, 140, 258, 0.85)', - 'File Upload': 'rgba(241, 107, 223, 0.85)' - }}; + // Hash function to generate consistent color from string + function hashCode(str) {{ + let hash = 0; + for (let i = 0; i < str.length; i++) {{ + const char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32bit integer + }} + return Math.abs(hash); + }} - const borderColorMap = {{ - 'SQL Injection': 'rgba(233, 105, 113, 1)', - 'XSS': 'rgba(240, 136, 62, 1)', - 'Directory Traversal': 'rgba(248, 150, 56, 1)', - 'Command Injection': 'rgba(229, 229, 16, 1)', - 'Path Traversal': 'rgba(123, 201, 71, 1)', - 'Malware': 'rgba(88, 166, 255, 1)', - 'Brute Force': 'rgba(79, 161, 246, 1)', - 'DDoS': 'rgba(139, 148, 244, 1)', - 'CSRF': 'rgba(188, 140, 258, 1)', - 'File Upload': 'rgba(241, 107, 223, 1)' - }}; + // Dynamic color generator based on hash + function generateColorFromHash(label) {{ + const hash = hashCode(label); + const hue = (hash % 360); // 0-360 for hue + const saturation = 70 + (hash % 20); // 70-90 for vibrant colors + const lightness = 50 + (hash % 10); // 50-60 for brightness + + const bgColor = `hsl(${{hue}}, ${{saturation}}%, ${{lightness}}%)`; + const borderColor = `hsl(${{hue}}, ${{saturation + 5}}%, ${{lightness - 10}}%)`; // Darker border + const hoverColor = `hsl(${{hue}}, ${{saturation - 10}}%, ${{lightness + 8}}%)`; // Lighter hover + + return {{ bg: bgColor, border: borderColor, hover: hoverColor }}; + }} - const hoverColorMap = {{ - 'SQL Injection': 'rgba(233, 105, 113, 1)', - 'XSS': 'rgba(240, 136, 62, 1)', - 'Directory Traversal': 'rgba(248, 150, 56, 1)', - 'Command Injection': 'rgba(229, 229, 16, 1)', - 'Path Traversal': 'rgba(123, 201, 71, 1)', - 'Malware': 'rgba(88, 166, 255, 1)', - 'Brute Force': 'rgba(79, 161, 246, 1)', - 'DDoS': 'rgba(139, 148, 244, 1)', - 'CSRF': 'rgba(188, 140, 258, 1)', - 'File Upload': 'rgba(241, 107, 223, 1)' - }}; - - const backgroundColors = labels.map(label => colorMap[label] || 'rgba(88, 166, 255, 0.85)'); - const borderColors = labels.map(label => borderColorMap[label] || 'rgba(88, 166, 255, 1)'); - const hoverColors = labels.map(label => hoverColorMap[label] || 'rgba(88, 166, 255, 1)'); + // Generate colors dynamically for each attack type + const backgroundColors = labels.map(label => generateColorFromHash(label).bg); + const borderColors = labels.map(label => generateColorFromHash(label).border); + const hoverColors = labels.map(label => generateColorFromHash(label).hover); // Create or update chart if (attackTypesChart) {{ @@ -3154,25 +3441,46 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str: const ctx = canvas.getContext('2d'); attackTypesChart = new Chart(ctx, {{ - type: 'bar', + type: 'doughnut', data: {{ labels: labels, datasets: [{{ data: counts, backgroundColor: backgroundColors, - borderColor: borderColors, - borderWidth: 2, - borderRadius: 6, - borderSkipped: false + borderColor: '#0d1117', + borderWidth: 3, + hoverBorderColor: '#58a6ff', + hoverBorderWidth: 4, + hoverOffset: 10 }}] }}, options: {{ responsive: true, maintainAspectRatio: false, - indexAxis: 'y', plugins: {{ legend: {{ - display: false + position: 'right', + labels: {{ + color: '#c9d1d9', + font: {{ + size: 12, + weight: '500', + family: "'Segoe UI', Tahoma, Geneva, Verdana" + }}, + padding: 16, + usePointStyle: true, + pointStyle: 'circle', + generateLabels: (chart) => {{ + const data = chart.data; + return data.labels.map((label, i) => ({{ + text: `${{label}} (${{data.datasets[0].data[i]}})`, + fillStyle: data.datasets[0].backgroundColor[i], + hidden: false, + index: i, + pointStyle: 'circle' + }})); + }} + }} }}, tooltip: {{ enabled: true, @@ -3182,7 +3490,6 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str: borderColor: '#58a6ff', borderWidth: 2, padding: 14, - displayColors: false, titleFont: {{ size: 14, weight: 'bold', @@ -3195,61 +3502,16 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str: caretSize: 8, caretPadding: 12, callbacks: {{ - title: function(context) {{ - return ''; - }}, label: function(context) {{ - return context.parsed.x; + const total = context.dataset.data.reduce((a, b) => a + b, 0); + const percentage = ((context.parsed / total) * 100).toFixed(1); + return `${{context.label}}: ${{percentage}}%`; }} }} }} }}, - scales: {{ - x: {{ - beginAtZero: true, - ticks: {{ - color: '#8b949e', - font: {{ - size: 12, - weight: '500' - }} - }}, - grid: {{ - color: 'rgba(48, 54, 61, 0.4)', - drawBorder: false, - drawTicks: false - }} - }}, - y: {{ - ticks: {{ - color: '#c9d1d9', - font: {{ - size: 13, - weight: '600' - }}, - padding: 12, - callback: function(value, index) {{ - const label = this.getLabelForValue(value); - const maxLength = 25; - return label.length > maxLength ? label.substring(0, maxLength) + '…' : label; - }} - }}, - grid: {{ - display: false, - drawBorder: false - }} - }} - }}, animation: {{ - duration: 1000, - easing: 'easeInOutQuart', - delay: (context) => {{ - let delay = 0; - if (context.type === 'data') {{ - delay = context.dataIndex * 50 + context.datasetIndex * 100; - }} - return delay; - }} + enabled: false }}, onHover: (event, activeElements) => {{ canvas.style.cursor = activeElements.length > 0 ? 'pointer' : 'default';