@@ -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';