Major updates: - Replace luci-app-netifyd-dashboard with enhanced luci-app-secubox-netifyd - Add netifyd 5.2.1 package with GCC 13.3/C++17 build fixes - Fix nd-risks.cpp compilation errors via inline static maps patch - Enhance local-build.sh with improved package building workflow - Update secubox-core scripts version to v0.9.1 New Features: - Complete netifyd dashboard with flows, devices, applications, and settings - Local data collection with netifyd-collector - Automated cron-based data aggregation - RPCd integration for real-time statistics Build Fixes: - Patch 001: Fix C++17 inline static maps in nd-risks.hpp and nd-protos.hpp - Patch 003: Skip ndpi tests to resolve roaring_v2 dependency issues - Add libatomic dependency - Include libnetifyd shared libraries in package 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
521 lines
17 KiB
JavaScript
521 lines
17 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require poll';
|
|
'require ui';
|
|
'require dom';
|
|
'require secubox-netifyd/api as netifydAPI';
|
|
|
|
return view.extend({
|
|
refreshInterval: 3,
|
|
flowsData: [],
|
|
statsData: {},
|
|
isPaused: false,
|
|
flowsContainer: null,
|
|
statsContainer: null,
|
|
filterProtocol: '',
|
|
filterApplication: '',
|
|
searchQuery: '',
|
|
|
|
load: function() {
|
|
return Promise.all([
|
|
netifydAPI.getRealtimeFlows(),
|
|
netifydAPI.getFlowStatistics()
|
|
]);
|
|
},
|
|
|
|
handlePauseResume: function(ev) {
|
|
this.isPaused = !this.isPaused;
|
|
var btn = ev.target.closest('button');
|
|
if (this.isPaused) {
|
|
btn.innerHTML = '<i class="fa fa-play"></i> ' + _('Resume');
|
|
btn.classList.remove('btn-warning');
|
|
btn.classList.add('btn-success');
|
|
} else {
|
|
btn.innerHTML = '<i class="fa fa-pause"></i> ' + _('Pause');
|
|
btn.classList.remove('btn-success');
|
|
btn.classList.add('btn-warning');
|
|
}
|
|
},
|
|
|
|
handleExport: function(format, ev) {
|
|
ui.showModal(_('Export Flows'), [
|
|
E('p', { 'class': 'spinning' }, _('Exporting flows to %s...').format(format.toUpperCase()))
|
|
]);
|
|
|
|
netifydAPI.exportFlows(format).then(function(result) {
|
|
ui.hideModal();
|
|
if (result.success) {
|
|
ui.addNotification(null, E('p', _('Flows exported to: %s').format(result.file)), 'info');
|
|
} else {
|
|
ui.addNotification(null, E('p', result.message || _('Export failed')), 'error');
|
|
}
|
|
}).catch(function(err) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', _('Error: %s').format(err.message)), 'error');
|
|
});
|
|
},
|
|
|
|
handleClearCache: function(ev) {
|
|
ui.showConfirmation(_('Clear Flow Cache'), _('Are you sure you want to clear the flow cache? This will remove all cached flow data.'), function() {
|
|
netifydAPI.clearCache().then(function(result) {
|
|
if (result.success) {
|
|
ui.addNotification(null, E('p', result.message || _('Cache cleared')), 'info');
|
|
window.location.reload();
|
|
} else {
|
|
ui.addNotification(null, E('p', result.message || _('Failed to clear cache')), 'error');
|
|
}
|
|
});
|
|
}.bind(this));
|
|
},
|
|
|
|
filterFlows: function(flows) {
|
|
var filtered = flows;
|
|
|
|
// Apply protocol filter
|
|
if (this.filterProtocol) {
|
|
filtered = filtered.filter(function(flow) {
|
|
return (flow.protocol || '').toLowerCase() === this.filterProtocol.toLowerCase();
|
|
}.bind(this));
|
|
}
|
|
|
|
// Apply application filter
|
|
if (this.filterApplication) {
|
|
filtered = filtered.filter(function(flow) {
|
|
return (flow.application || flow.app || '').toLowerCase() === this.filterApplication.toLowerCase();
|
|
}.bind(this));
|
|
}
|
|
|
|
// Apply search query
|
|
if (this.searchQuery) {
|
|
var query = this.searchQuery.toLowerCase();
|
|
filtered = filtered.filter(function(flow) {
|
|
var srcIp = (flow.ip_orig || flow.src_ip || '').toLowerCase();
|
|
var dstIp = (flow.ip_resp || flow.dst_ip || '').toLowerCase();
|
|
var app = (flow.application || flow.app || '').toLowerCase();
|
|
var proto = (flow.protocol || '').toLowerCase();
|
|
return srcIp.indexOf(query) >= 0 || dstIp.indexOf(query) >= 0 ||
|
|
app.indexOf(query) >= 0 || proto.indexOf(query) >= 0;
|
|
}.bind(this));
|
|
}
|
|
|
|
return filtered;
|
|
},
|
|
|
|
renderToolbar: function(flowsData) {
|
|
var self = this;
|
|
|
|
// Get unique protocols and applications for filters
|
|
var protocols = {};
|
|
var applications = {};
|
|
(flowsData.flows || []).forEach(function(flow) {
|
|
var proto = flow.protocol || 'Unknown';
|
|
var app = flow.application || flow.app || 'Unknown';
|
|
protocols[proto] = (protocols[proto] || 0) + 1;
|
|
applications[app] = (applications[app] || 0) + 1;
|
|
});
|
|
|
|
return E('div', {
|
|
'class': 'cbi-section-node',
|
|
'style': 'background: #f9fafb; padding: 1rem; border-radius: 8px; margin-bottom: 1rem'
|
|
}, [
|
|
E('div', {
|
|
'style': 'display: grid; grid-template-columns: 1fr 1fr 1fr auto; gap: 0.75rem; align-items: center'
|
|
}, [
|
|
// Search
|
|
E('div', [
|
|
E('label', { 'style': 'font-size: 0.9em; display: block; margin-bottom: 0.25rem' }, [
|
|
E('i', { 'class': 'fa fa-search' }),
|
|
' ',
|
|
_('Search')
|
|
]),
|
|
E('input', {
|
|
'type': 'text',
|
|
'class': 'cbi-input-text',
|
|
'placeholder': _('IP, app, protocol...'),
|
|
'style': 'width: 100%',
|
|
'value': this.searchQuery,
|
|
'keyup': function(ev) {
|
|
self.searchQuery = ev.target.value;
|
|
self.updateFlowsTable();
|
|
}
|
|
})
|
|
]),
|
|
|
|
// Protocol Filter
|
|
E('div', [
|
|
E('label', { 'style': 'font-size: 0.9em; display: block; margin-bottom: 0.25rem' }, [
|
|
E('i', { 'class': 'fa fa-filter' }),
|
|
' ',
|
|
_('Protocol')
|
|
]),
|
|
E('select', {
|
|
'class': 'cbi-input-select',
|
|
'style': 'width: 100%',
|
|
'change': function(ev) {
|
|
self.filterProtocol = ev.target.value;
|
|
self.updateFlowsTable();
|
|
}
|
|
}, [
|
|
E('option', { 'value': '' }, _('All Protocols')),
|
|
Object.keys(protocols).sort().map(function(proto) {
|
|
return E('option', { 'value': proto }, proto + ' (' + protocols[proto] + ')');
|
|
})
|
|
])
|
|
]),
|
|
|
|
// Application Filter
|
|
E('div', [
|
|
E('label', { 'style': 'font-size: 0.9em; display: block; margin-bottom: 0.25rem' }, [
|
|
E('i', { 'class': 'fa fa-filter' }),
|
|
' ',
|
|
_('Application')
|
|
]),
|
|
E('select', {
|
|
'class': 'cbi-input-select',
|
|
'style': 'width: 100%',
|
|
'change': function(ev) {
|
|
self.filterApplication = ev.target.value;
|
|
self.updateFlowsTable();
|
|
}
|
|
}, [
|
|
E('option', { 'value': '' }, _('All Applications')),
|
|
Object.keys(applications).sort().map(function(app) {
|
|
return E('option', { 'value': app }, app + ' (' + applications[app] + ')');
|
|
})
|
|
])
|
|
]),
|
|
|
|
// Actions
|
|
E('div', [
|
|
E('label', { 'style': 'font-size: 0.9em; display: block; margin-bottom: 0.25rem; opacity: 0' }, '-'),
|
|
E('div', { 'class': 'btn-group' }, [
|
|
E('button', {
|
|
'class': 'btn btn-sm btn-warning',
|
|
'click': ui.createHandlerFn(this, 'handlePauseResume')
|
|
}, [
|
|
E('i', { 'class': 'fa fa-pause' }),
|
|
' ',
|
|
_('Pause')
|
|
]),
|
|
E('button', {
|
|
'class': 'btn btn-sm btn-primary',
|
|
'click': ui.createHandlerFn(this, 'handleExport', 'json')
|
|
}, [
|
|
E('i', { 'class': 'fa fa-download' }),
|
|
' ',
|
|
_('Export')
|
|
]),
|
|
E('button', {
|
|
'class': 'btn btn-sm btn-danger',
|
|
'click': ui.createHandlerFn(this, 'handleClearCache')
|
|
}, [
|
|
E('i', { 'class': 'fa fa-trash' })
|
|
])
|
|
])
|
|
])
|
|
]),
|
|
|
|
// Info bar
|
|
E('div', {
|
|
'class': 'alert alert-info',
|
|
'style': 'margin-top: 0.75rem; margin-bottom: 0; padding: 0.5rem 1rem; display: flex; align-items: center; gap: 1rem; flex-wrap: wrap'
|
|
}, [
|
|
E('span', [
|
|
E('i', { 'class': 'fa fa-circle', 'style': 'color: ' + (flowsData.source === 'socket' ? '#10b981' : '#f59e0b') }),
|
|
' ',
|
|
E('strong', _('Source:')),
|
|
' ',
|
|
flowsData.source === 'socket' ? _('Live Socket') : _('Cached Dump')
|
|
]),
|
|
E('span', [
|
|
E('i', { 'class': 'fa fa-sync' }),
|
|
' ',
|
|
E('strong', _('Refresh:')),
|
|
' ',
|
|
_('Every %d seconds').format(this.refreshInterval)
|
|
]),
|
|
E('span', [
|
|
E('i', { 'class': 'fa fa-database' }),
|
|
' ',
|
|
E('strong', _('Total Flows:')),
|
|
' ',
|
|
(flowsData.flows || []).length
|
|
])
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderStatsSummary: function(stats) {
|
|
if (!stats) return null;
|
|
|
|
var rateInMbps = ((stats.rate_bytes_in || 0) * 8 / 1000000).toFixed(2);
|
|
var rateOutMbps = ((stats.rate_bytes_out || 0) * 8 / 1000000).toFixed(2);
|
|
|
|
var statItems = [
|
|
{
|
|
label: _('Total Flows'),
|
|
value: (stats.total_flows || 0).toString(),
|
|
icon: 'exchange-alt',
|
|
color: '#3b82f6'
|
|
},
|
|
{
|
|
label: _('Downloaded'),
|
|
value: netifydAPI.formatBytes(stats.total_bytes_in || 0),
|
|
icon: 'download',
|
|
color: '#10b981'
|
|
},
|
|
{
|
|
label: _('Uploaded'),
|
|
value: netifydAPI.formatBytes(stats.total_bytes_out || 0),
|
|
icon: 'upload',
|
|
color: '#f59e0b'
|
|
},
|
|
{
|
|
label: _('Download Rate'),
|
|
value: rateInMbps + ' Mbps',
|
|
icon: 'arrow-down',
|
|
color: '#14b8a6'
|
|
},
|
|
{
|
|
label: _('Upload Rate'),
|
|
value: rateOutMbps + ' Mbps',
|
|
icon: 'arrow-up',
|
|
color: '#ef4444'
|
|
}
|
|
];
|
|
|
|
return E('div', {
|
|
'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 1.5rem'
|
|
}, statItems.map(function(item) {
|
|
return E('div', {
|
|
'style': 'background: linear-gradient(135deg, ' + item.color + '22 0%, ' + item.color + '11 100%); border-left: 4px solid ' + item.color + '; padding: 1rem; border-radius: 6px'
|
|
}, [
|
|
E('div', { 'style': 'font-size: 0.85em; color: #6b7280; margin-bottom: 0.5rem; display: flex; align-items: center; gap: 0.5rem' }, [
|
|
E('i', { 'class': 'fa fa-' + item.icon, 'style': 'color: ' + item.color }),
|
|
item.label
|
|
]),
|
|
E('div', { 'style': 'font-size: 1.5em; font-weight: bold; color: ' + item.color }, item.value)
|
|
]);
|
|
}));
|
|
},
|
|
|
|
renderFlowsTable: function(flows) {
|
|
if (!flows || flows.length === 0) {
|
|
return E('div', {
|
|
'class': 'alert-message info',
|
|
'style': 'text-align: center; padding: 3rem'
|
|
}, [
|
|
E('i', { 'class': 'fa fa-stream', 'style': 'font-size: 3em; opacity: 0.3; display: block; margin-bottom: 1rem' }),
|
|
E('h4', _('No Active Flows')),
|
|
E('p', { 'class': 'text-muted' }, _('Waiting for network traffic to be detected...')),
|
|
E('small', _('Make sure Netifyd service is running and capturing traffic'))
|
|
]);
|
|
}
|
|
|
|
// Apply filters
|
|
var filteredFlows = this.filterFlows(flows);
|
|
|
|
// Limit display for performance
|
|
var displayFlows = filteredFlows.slice(0, 200);
|
|
|
|
return E('div', [
|
|
E('div', { 'class': 'table', 'style': 'font-size: 0.9em' }, [
|
|
// Header
|
|
E('div', { 'class': 'tr table-titles' }, [
|
|
E('div', { 'class': 'th', 'style': 'width: 18%' }, _('Source')),
|
|
E('div', { 'class': 'th', 'style': 'width: 18%' }, _('Destination')),
|
|
E('div', { 'class': 'th center', 'style': 'width: 12%' }, _('Protocol')),
|
|
E('div', { 'class': 'th center', 'style': 'width: 15%' }, _('Application')),
|
|
E('div', { 'class': 'th right', 'style': 'width: 12%' }, _('Traffic')),
|
|
E('div', { 'class': 'th center', 'style': 'width: 10%' }, _('Packets')),
|
|
E('div', { 'class': 'th center', 'style': 'width: 10%' }, _('Duration'))
|
|
]),
|
|
|
|
// Rows
|
|
displayFlows.map(function(flow, idx) {
|
|
var srcIp = flow.ip_orig || flow.src_ip || 'N/A';
|
|
var dstIp = flow.ip_resp || flow.dst_ip || 'N/A';
|
|
var srcPort = flow.port_orig || flow.src_port || '';
|
|
var dstPort = flow.port_resp || flow.dst_port || '';
|
|
var protocol = flow.protocol || 'Unknown';
|
|
var application = flow.application || flow.app || 'Unknown';
|
|
var bytes = (flow.bytes_orig || 0) + (flow.bytes_resp || 0);
|
|
var packets = (flow.packets_orig || 0) + (flow.packets_resp || 0);
|
|
var duration = flow.duration || 0;
|
|
|
|
// Protocol colors
|
|
var protoColors = {
|
|
'TCP': '#3b82f6',
|
|
'UDP': '#10b981',
|
|
'ICMP': '#f59e0b',
|
|
'HTTP': '#14b8a6',
|
|
'HTTPS': '#8b5cf6'
|
|
};
|
|
var protoColor = protoColors[protocol] || '#6b7280';
|
|
|
|
return E('div', { 'class': 'tr', 'style': idx % 2 === 0 ? 'background: #f9fafb' : '' }, [
|
|
E('div', { 'class': 'td', 'style': 'width: 18%' }, [
|
|
E('code', { 'style': 'font-size: 0.85em' }, srcIp),
|
|
srcPort ? E('span', { 'class': 'text-muted', 'style': 'font-size: 0.8em' }, ':' + srcPort) : ''
|
|
]),
|
|
E('div', { 'class': 'td', 'style': 'width: 18%' }, [
|
|
E('code', { 'style': 'font-size: 0.85em' }, dstIp),
|
|
dstPort ? E('span', { 'class': 'text-muted', 'style': 'font-size: 0.8em' }, ':' + dstPort) : ''
|
|
]),
|
|
E('div', { 'class': 'td center', 'style': 'width: 12%' }, [
|
|
E('span', {
|
|
'class': 'badge',
|
|
'style': 'background: ' + protoColor + '; color: white; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75em'
|
|
}, protocol)
|
|
]),
|
|
E('div', { 'class': 'td center', 'style': 'width: 15%' }, [
|
|
E('span', {
|
|
'class': 'badge',
|
|
'style': 'background: #6366f1; color: white; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75em'
|
|
}, application)
|
|
]),
|
|
E('div', { 'class': 'td right', 'style': 'width: 12%' },
|
|
netifydAPI.formatBytes(bytes)),
|
|
E('div', { 'class': 'td center', 'style': 'width: 10%' },
|
|
packets.toString()),
|
|
E('div', { 'class': 'td center', 'style': 'width: 10%' },
|
|
duration > 0 ? netifydAPI.formatDuration(duration) : '-')
|
|
]);
|
|
}.bind(this))
|
|
]),
|
|
|
|
// Pagination info
|
|
filteredFlows.length > 200 ? E('div', {
|
|
'class': 'alert alert-warning',
|
|
'style': 'margin-top: 1rem; text-align: center'
|
|
}, _('Showing 200 of %d flows. Use filters to narrow results.').format(filteredFlows.length)) : null,
|
|
|
|
filteredFlows.length === 0 && flows.length > 0 ? E('div', {
|
|
'class': 'alert alert-info',
|
|
'style': 'margin-top: 1rem; text-align: center'
|
|
}, _('No flows match the current filters')) : null
|
|
]);
|
|
},
|
|
|
|
updateFlowsTable: function() {
|
|
if (this.flowsContainer && this.flowsData) {
|
|
dom.content(this.flowsContainer, this.renderFlowsTable(this.flowsData));
|
|
}
|
|
},
|
|
|
|
render: function(data) {
|
|
var flowsData = data[0] || { flows: [] };
|
|
var statsData = data[1] || {};
|
|
|
|
// Store data
|
|
this.flowsData = flowsData.flows || [];
|
|
this.statsData = statsData;
|
|
|
|
var self = this;
|
|
|
|
// Set up polling
|
|
poll.add(L.bind(function() {
|
|
if (this.isPaused) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
return Promise.all([
|
|
netifydAPI.getRealtimeFlows(),
|
|
netifydAPI.getFlowStatistics()
|
|
]).then(L.bind(function(result) {
|
|
this.flowsData = (result[0] || {}).flows || [];
|
|
this.statsData = result[1] || {};
|
|
|
|
// Update containers
|
|
if (self.flowsContainer) {
|
|
dom.content(self.flowsContainer, self.renderFlowsTable(self.flowsData));
|
|
}
|
|
if (self.statsContainer) {
|
|
dom.content(self.statsContainer, self.renderStatsSummary(self.statsData));
|
|
}
|
|
}, this));
|
|
}, this), this.refreshInterval);
|
|
|
|
return E('div', { 'class': 'cbi-map' }, [
|
|
E('h2', { 'name': 'content' }, [
|
|
E('i', { 'class': 'fa fa-stream', 'style': 'margin-right: 0.5rem' }),
|
|
_('Real-Time Network Flows')
|
|
]),
|
|
E('div', { 'class': 'cbi-map-descr' },
|
|
_('Live monitoring of network flows detected by Netifyd DPI engine with filtering and search capabilities')),
|
|
|
|
// Information banner when no flow data
|
|
(!this.flowsData || this.flowsData.length === 0) ? E('div', {
|
|
'class': 'alert-message',
|
|
'style': 'background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); color: white; padding: 1.5rem; margin: 1rem 0; border-radius: 8px'
|
|
}, [
|
|
E('div', { 'style': 'display: flex; align-items: flex-start; gap: 1rem' }, [
|
|
E('i', { 'class': 'fa fa-info-circle', 'style': 'font-size: 2em; margin-top: 0.25rem' }),
|
|
E('div', { 'style': 'flex: 1' }, [
|
|
E('h4', { 'style': 'margin: 0 0 0.5rem 0; font-size: 1.1em' }, _('Detailed Flow Data Not Available')),
|
|
E('p', { 'style': 'margin: 0 0 0.75rem 0; opacity: 0.95' },
|
|
_('Netifyd is tracking flows but detailed flow information is not being exported to SecuBox dashboard. Flow count and statistics are available, but individual flow details (IPs, protocols, bytes) are not.')),
|
|
E('p', { 'style': 'margin: 0; font-size: 0.9em' }, [
|
|
E('strong', _('Options:')),
|
|
E('br'),
|
|
_('1. Access full flow details via '),
|
|
E('a', {
|
|
'href': 'https://dashboard.netify.ai',
|
|
'target': '_blank',
|
|
'style': 'color: white; text-decoration: underline'
|
|
}, _('Netify.ai Cloud Dashboard')),
|
|
E('br'),
|
|
_('2. Configure local flow export (see '),
|
|
E('code', { 'style': 'background: rgba(0,0,0,0.2); padding: 0.2rem 0.4rem; border-radius: 3px' }, 'README-FLOW-DATA.md'),
|
|
_(' for instructions)')
|
|
])
|
|
])
|
|
])
|
|
]) : null,
|
|
|
|
E('div', { 'class': 'cbi-section' }, [
|
|
this.renderToolbar(flowsData)
|
|
]),
|
|
|
|
E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', [
|
|
E('i', { 'class': 'fa fa-chart-bar', 'style': 'margin-right: 0.5rem' }),
|
|
_('Flow Statistics')
|
|
]),
|
|
E('div', { 'class': 'cbi-section-node' }, [
|
|
self.statsContainer = E('div')
|
|
])
|
|
]),
|
|
|
|
E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', [
|
|
E('i', { 'class': 'fa fa-list', 'style': 'margin-right: 0.5rem' }),
|
|
_('Active Flows'),
|
|
' ',
|
|
E('span', {
|
|
'class': 'badge',
|
|
'style': 'background: #3b82f6; color: white; margin-left: 0.5rem'
|
|
}, this.flowsData.length)
|
|
]),
|
|
E('div', { 'class': 'cbi-section-node' }, [
|
|
self.flowsContainer = E('div')
|
|
])
|
|
])
|
|
]);
|
|
},
|
|
|
|
addFooter: function() {
|
|
// Initial render of dynamic containers
|
|
if (this.statsContainer) {
|
|
dom.content(this.statsContainer, this.renderStatsSummary(this.statsData));
|
|
}
|
|
if (this.flowsContainer) {
|
|
dom.content(this.flowsContainer, this.renderFlowsTable(this.flowsData));
|
|
}
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|