Initialize project and add Netify API integration with TDD workflow
This commit is contained in:
1 parent
8a077cf692
commit
da8fb57bfb
43 files changed
+9228
-113
No files matched your search
@@ -39,3 +39,7 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# local databases & temporary files
|
||||
backend/netify_data.db*
|
||||
temp.json
|
||||
+1079
File diff suppressed because it is too large.
Load diff
+1476
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,293 @@
|
||||
// backend/routes/dashboard.js
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../database');
|
||||
const { runPoll } = require('../scheduler');
|
||||
|
||||
// GET /api/dashboard/summary — kartu ringkasan (top of page)
|
||||
router.get('/summary', (req, res) => {
|
||||
const stats = db.getStats();
|
||||
const timeline = db.getBandwidthTimeline(1);
|
||||
const latest = timeline[0] ?? {};
|
||||
|
||||
res.json({
|
||||
ok: true,
|
||||
data: {
|
||||
total_devices : stats.totalDevices,
|
||||
total_threats : stats.totalThreats,
|
||||
total_events : stats.totalEvents,
|
||||
last_fetch : stats.lastFetch,
|
||||
bandwidth_down : latest.total_download ?? 0,
|
||||
bandwidth_up : latest.total_upload ?? 0,
|
||||
active_flows : stats.activeFlows,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// GET /api/dashboard/apps — top aplikasi
|
||||
router.get('/apps', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 10);
|
||||
const data = db.getLatestBandwidthApps(limit);
|
||||
res.json({ ok: true, data });
|
||||
});
|
||||
|
||||
// GET /api/dashboard/devices — daftar device
|
||||
router.get('/devices', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 50);
|
||||
const data = db.getLatestDevices(limit);
|
||||
res.json({ ok: true, data });
|
||||
});
|
||||
|
||||
// GET /api/dashboard/flows — flow aktif
|
||||
router.get('/flows', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 50);
|
||||
const data = db.getLatestFlows(limit);
|
||||
res.json({ ok: true, data });
|
||||
});
|
||||
|
||||
// GET /api/dashboard/threats — ancaman keamanan
|
||||
router.get('/threats', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 20);
|
||||
const data = db.getLatestThreats(limit);
|
||||
res.json({ ok: true, data });
|
||||
});
|
||||
|
||||
// GET /api/dashboard/protocols — top protokol
|
||||
router.get('/protocols', (req, res) => {
|
||||
const data = db.getLatestProtocols();
|
||||
res.json({ ok: true, data });
|
||||
});
|
||||
|
||||
// GET /api/dashboard/countries — top negara
|
||||
router.get('/countries', (req, res) => {
|
||||
const data = db.getLatestCountries();
|
||||
res.json({ ok: true, data });
|
||||
});
|
||||
|
||||
// GET /api/dashboard/dns — top DNS queries
|
||||
router.get('/dns', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 20);
|
||||
const data = db.getLatestDNS(limit);
|
||||
res.json({ ok: true, data });
|
||||
});
|
||||
|
||||
// GET /api/dashboard/events — events terbaru
|
||||
router.get('/events', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 20);
|
||||
const rawData = db.getLatestEvents(limit);
|
||||
const mappedData = rawData.map(r => ({
|
||||
id: r.id,
|
||||
event_id: r.event_id,
|
||||
event_type: r.event_type || 'unknown',
|
||||
severity: r.severity || 'info',
|
||||
message: r.description || '',
|
||||
source_ip: r.ip_address || null,
|
||||
timestamp: r.event_at || r.fetched_at || new Date().toISOString()
|
||||
}));
|
||||
res.json({ ok: true, data: mappedData });
|
||||
});
|
||||
|
||||
// GET /api/dashboard/timeline — bandwidth timeline (grafik)
|
||||
router.get('/timeline', (req, res) => {
|
||||
const points = parseInt(req.query.points ?? 60);
|
||||
const data = db.getBandwidthTimeline(points);
|
||||
res.json({ ok: true, data });
|
||||
});
|
||||
|
||||
// POST /api/dashboard/refresh — trigger manual poll
|
||||
router.post('/refresh', async (req, res) => {
|
||||
await runPoll();
|
||||
res.json({ ok: true, message: 'Poll berhasil dijalankan.' });
|
||||
});
|
||||
|
||||
// ─── ROUTES FITUR BARU 1-11 ───────────────────────────────────────────────────
|
||||
router.get('/app-categories', (req, res) => {
|
||||
res.json({ ok: true, data: db.getLatest('app_categories', 'download', 15) });
|
||||
});
|
||||
router.get('/continents', (req, res) => {
|
||||
res.json({ ok: true, data: db.getLatest('continents', 'download', 10) });
|
||||
});
|
||||
router.get('/regions', (req, res) => {
|
||||
res.json({ ok: true, data: db.getLatest('regions', 'download', 20) });
|
||||
});
|
||||
router.get('/cities', (req, res) => {
|
||||
res.json({ ok: true, data: db.getLatest('cities', 'download', 20) });
|
||||
});
|
||||
router.get('/vlans', (req, res) => {
|
||||
res.json({ ok: true, data: db.getLatest('vlans', 'download', 20) });
|
||||
});
|
||||
router.get('/interfaces', (req, res) => {
|
||||
res.json({ ok: true, data: db.getLatest('interfaces', 'download', 20) });
|
||||
});
|
||||
router.get('/flow-types', (req, res) => {
|
||||
res.json({ ok: true, data: db.getLatest('flow_types', 'download', 10) });
|
||||
});
|
||||
router.get('/flow-origins', (req, res) => {
|
||||
res.json({ ok: true, data: db.getLatest('flow_origins', 'download', 10) });
|
||||
});
|
||||
router.get('/ip-versions', (req, res) => {
|
||||
res.json({ ok: true, data: db.getLatest('ip_versions', 'download', 5) });
|
||||
});
|
||||
router.get('/remote-ips', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 20);
|
||||
res.json({ ok: true, data: db.getLatest('remote_ips', 'download', limit) });
|
||||
});
|
||||
router.get('/mac-bandwidth', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 50);
|
||||
res.json({ ok: true, data: db.getLatest('mac_bandwidth', 'download', limit) });
|
||||
});
|
||||
|
||||
// ─── FIX: ROUTES YANG SEBELUMNYA HILANG ──────────────────────────────────────
|
||||
|
||||
// TLS Versions
|
||||
router.get('/tls-versions', (req, res) => {
|
||||
res.json({ ok: true, data: db.getLatest('tls_versions', 'download', 10) });
|
||||
});
|
||||
|
||||
// TLS Ciphers
|
||||
router.get('/tls-ciphers', (req, res) => {
|
||||
res.json({ ok: true, data: db.getLatest('tls_ciphers', 'download', 15) });
|
||||
});
|
||||
|
||||
// TLS Security Level
|
||||
router.get('/tls-security', (req, res) => {
|
||||
res.json({ ok: true, data: db.getLatest('tls_security', 'download', 10) });
|
||||
});
|
||||
|
||||
// NetBIOS Hostnames (Windows devices)
|
||||
router.get('/netbios', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 30);
|
||||
res.json({ ok: true, data: db.getLatest('netbios_hostnames', 'download', limit) });
|
||||
});
|
||||
|
||||
// Discovery OS (sistem operasi yang terdeteksi)
|
||||
router.get('/discovery-os', (req, res) => {
|
||||
res.json({ ok: true, data: db.getLatest('discovery_os', 'download', 20) });
|
||||
});
|
||||
|
||||
// ─── ROUTES DPI 12-21 ─────────────────────────────────────────────────────────
|
||||
|
||||
// 12. DHCP Class Fingerprint
|
||||
router.get('/dhcp-fingerprints', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 30);
|
||||
res.json({ ok: true, data: db.getLatest('dhcp_fingerprints', 'download', limit) });
|
||||
});
|
||||
|
||||
// 13. HTTP User-Agent
|
||||
router.get('/http-user-agents', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 30);
|
||||
res.json({ ok: true, data: db.getLatest('http_user_agents', 'download', limit) });
|
||||
});
|
||||
|
||||
// 14. HTTPS SNI Hostname
|
||||
router.get('/sni-hostnames', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 30);
|
||||
res.json({ ok: true, data: db.getLatest('sni_hostnames', 'download', limit) });
|
||||
});
|
||||
|
||||
// 15. SSL Server Common Name
|
||||
router.get('/ssl-server-cn', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 30);
|
||||
res.json({ ok: true, data: db.getLatest('ssl_server_cn', 'download', limit) });
|
||||
});
|
||||
|
||||
// 17. QUIC Hostname
|
||||
router.get('/quic-hostnames', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 30);
|
||||
res.json({ ok: true, data: db.getLatest('quic_hostnames', 'download', limit) });
|
||||
});
|
||||
|
||||
// 18. BitTorrent Info Hash
|
||||
router.get('/bittorrent-hashes', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 30);
|
||||
res.json({ ok: true, data: db.getLatest('bittorrent_hashes', 'download', limit) });
|
||||
});
|
||||
|
||||
// 19. SSH Version
|
||||
router.get('/ssh-versions', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 20);
|
||||
res.json({ ok: true, data: db.getLatest('ssh_versions', 'download', limit) });
|
||||
});
|
||||
|
||||
// 21. mDNS Hostname (Chromecast, Apple TV, etc.)
|
||||
router.get('/mdns-hostnames', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 30);
|
||||
res.json({ ok: true, data: db.getLatest('mdns_hostnames', 'download', limit) });
|
||||
});
|
||||
|
||||
// ─── INTELLIGENCE ROUTES 22-30 ────────────────────────────────────────────────
|
||||
|
||||
// Stats ringkasan semua intelligence (untuk badge count di tab)
|
||||
router.get('/intelligence/stats', (req, res) => {
|
||||
res.json({ ok: true, data: db.getIntelStats() });
|
||||
});
|
||||
|
||||
// 22. Cryptocurrency Mining
|
||||
router.get('/intelligence/crypto-mining', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 50);
|
||||
res.json({ ok: true, data: db.getIntelData('intel_crypto_mining', limit) });
|
||||
});
|
||||
|
||||
// 23. Device Discovery
|
||||
router.get('/intelligence/device-discovery', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 100);
|
||||
res.json({ ok: true, data: db.getIntelData('intel_device_discovery', limit) });
|
||||
});
|
||||
|
||||
// 24. Encryption Audit
|
||||
router.get('/intelligence/encryption-audit', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 50);
|
||||
res.json({ ok: true, data: db.getIntelData('intel_encryption_audit', limit) });
|
||||
});
|
||||
|
||||
// 25. Insecure Protocols
|
||||
router.get('/intelligence/insecure-protocols', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 50);
|
||||
res.json({ ok: true, data: db.getIntelData('intel_insecure_protocols', limit) });
|
||||
});
|
||||
|
||||
// 26. IP Reputation
|
||||
router.get('/intelligence/ip-reputation', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 50);
|
||||
res.json({ ok: true, data: db.getIntelData('intel_ip_reputation', limit) });
|
||||
});
|
||||
|
||||
// 27. Server Discovery
|
||||
router.get('/intelligence/server-discovery', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 100);
|
||||
res.json({ ok: true, data: db.getIntelData('intel_server_discovery', limit) });
|
||||
});
|
||||
|
||||
// 28. Tor Detection
|
||||
router.get('/intelligence/tor', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 50);
|
||||
res.json({ ok: true, data: db.getIntelData('intel_tor_detection', limit) });
|
||||
});
|
||||
|
||||
// 29. Unencrypted Passwords
|
||||
router.get('/intelligence/unencrypted-passwords', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 50);
|
||||
res.json({ ok: true, data: db.getIntelData('intel_unencrypted_passwords', limit) });
|
||||
});
|
||||
|
||||
// 30. VPN Detection
|
||||
router.get('/intelligence/vpn', (req, res) => {
|
||||
const limit = parseInt(req.query.limit ?? 50);
|
||||
res.json({ ok: true, data: db.getIntelData('intel_vpn_detection', limit) });
|
||||
});
|
||||
|
||||
// ─── LOOKUP ROUTES ────────────────────────────────────────────────────────────
|
||||
router.get('/lookup/applications', async (req, res) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page ?? 1);
|
||||
const limit = parseInt(req.query.limit ?? 50);
|
||||
const search = String(req.query.search ?? '');
|
||||
const { fetchLookupApplications } = require('../netify');
|
||||
const data = await fetchLookupApplications(page, limit, search);
|
||||
res.json({ ok: true, data });
|
||||
} catch (err) {
|
||||
res.status(500).json({ ok: false, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,282 @@
|
||||
// backend/scheduler.js
|
||||
const cron = require('node-cron');
|
||||
const netify = require('./netify');
|
||||
const db = require('./database');
|
||||
|
||||
let isRunning = false;
|
||||
|
||||
async function runPoll() {
|
||||
if (isRunning) {
|
||||
console.log('[Scheduler] Poll sedang berjalan, skip.');
|
||||
return;
|
||||
}
|
||||
isRunning = true;
|
||||
const fetchedAt = new Date().toISOString();
|
||||
console.log(`[Scheduler] Mulai polling... (${fetchedAt})`);
|
||||
|
||||
try {
|
||||
// 1. Top Aplikasi
|
||||
const apps = await netify.fetchTopApps(1440, 20);
|
||||
if (apps && Array.isArray(apps)) {
|
||||
db.insertBandwidthApps(apps, fetchedAt);
|
||||
console.log(`[Scheduler] OK Apps : ${apps.length} baris`);
|
||||
} else {
|
||||
console.log(`[Scheduler] -- Apps : tidak ada data`);
|
||||
}
|
||||
|
||||
// 2. Top Devices — pakai fetchDiscoveredDevices yg sudah dinormalisasi
|
||||
const devices = await netify.fetchDiscoveredDevices(1440, 200);
|
||||
if (devices && Array.isArray(devices)) {
|
||||
db.insertDevices(devices, fetchedAt);
|
||||
console.log(`[Scheduler] OK Devices : ${devices.length} baris`);
|
||||
} else {
|
||||
console.log(`[Scheduler] -- Devices : tidak ada data`);
|
||||
}
|
||||
|
||||
// 3. Top Protokol
|
||||
const protocols = await netify.fetchTopProtocols(1440, 20);
|
||||
if (protocols && Array.isArray(protocols)) {
|
||||
db.insertProtocols(protocols, fetchedAt);
|
||||
console.log(`[Scheduler] OK Protocols : ${protocols.length} baris`);
|
||||
} else {
|
||||
console.log(`[Scheduler] -- Protocols : tidak ada data`);
|
||||
}
|
||||
|
||||
// 4. Top Negara
|
||||
const countries = await netify.fetchTopCountries(1440, 15);
|
||||
if (countries && Array.isArray(countries)) {
|
||||
db.insertCountries(countries, fetchedAt);
|
||||
console.log(`[Scheduler] OK Countries : ${countries.length} baris`);
|
||||
} else {
|
||||
console.log(`[Scheduler] -- Countries : tidak ada data`);
|
||||
}
|
||||
|
||||
// 5. Top Domain/DNS
|
||||
const domains = await netify.fetchTopDomains(1440, 20);
|
||||
if (domains && Array.isArray(domains)) {
|
||||
db.insertDNS(domains, fetchedAt);
|
||||
console.log(`[Scheduler] OK DNS : ${domains.length} baris`);
|
||||
} else {
|
||||
console.log(`[Scheduler] -- DNS : tidak ada data`);
|
||||
}
|
||||
|
||||
// 6. Flows — pakai local_ip sebagai proxy
|
||||
const flows = await netify.fetchFlows(200);
|
||||
if (flows && Array.isArray(flows)) {
|
||||
db.insertFlows(flows, fetchedAt);
|
||||
console.log(`[Scheduler] OK Flows : ${flows.length} baris`);
|
||||
} else {
|
||||
console.log(`[Scheduler] -- Flows : tidak ada data`);
|
||||
}
|
||||
|
||||
// 7. Threats — dari Events Status
|
||||
const threats = await netify.fetchCyberThreats(1440, 50);
|
||||
if (threats && Array.isArray(threats)) {
|
||||
db.insertThreats(threats, fetchedAt);
|
||||
console.log(`[Scheduler] OK Threats : ${threats.length} baris`);
|
||||
} else {
|
||||
console.log(`[Scheduler] -- Threats : tidak ada data`);
|
||||
}
|
||||
|
||||
// 8. Events Log
|
||||
const events = await netify.fetchEvents(50);
|
||||
if (events && Array.isArray(events)) {
|
||||
db.insertEvents(events, fetchedAt);
|
||||
console.log(`[Scheduler] OK Events : ${events.length} baris`);
|
||||
} else {
|
||||
console.log(`[Scheduler] -- Events : tidak ada data`);
|
||||
}
|
||||
|
||||
// 10. App Categories
|
||||
const appCats = await netify.fetchTopAppCategories(1440, 15);
|
||||
if (appCats?.length) { db.insertAppCategories(appCats, fetchedAt); console.log(`[Scheduler] OK AppCats : ${appCats.length} baris`); }
|
||||
else console.log(`[Scheduler] -- AppCats : tidak ada data`);
|
||||
|
||||
// 11. Continents
|
||||
const continents = await netify.fetchTopContinents(1440, 10);
|
||||
if (continents?.length) { db.insertContinents(continents, fetchedAt); console.log(`[Scheduler] OK Continents : ${continents.length} baris`); }
|
||||
else console.log(`[Scheduler] -- Continents : tidak ada data`);
|
||||
|
||||
// 12. Regions
|
||||
const regions = await netify.fetchTopRegions(1440, 20);
|
||||
if (regions?.length) { db.insertRegions(regions, fetchedAt); console.log(`[Scheduler] OK Regions : ${regions.length} baris`); }
|
||||
else console.log(`[Scheduler] -- Regions : tidak ada data`);
|
||||
|
||||
// 13. Cities
|
||||
const cities = await netify.fetchTopCities(1440, 20);
|
||||
if (cities?.length) { db.insertCities(cities, fetchedAt); console.log(`[Scheduler] OK Cities : ${cities.length} baris`); }
|
||||
else console.log(`[Scheduler] -- Cities : tidak ada data`);
|
||||
|
||||
// 14. VLANs
|
||||
const vlans = await netify.fetchTopVLANs(1440, 20);
|
||||
if (vlans?.length) { db.insertVLANs(vlans, fetchedAt); console.log(`[Scheduler] OK VLANs : ${vlans.length} baris`); }
|
||||
else console.log(`[Scheduler] -- VLANs : tidak ada data`);
|
||||
|
||||
// 15. Interfaces
|
||||
const ifaces = await netify.fetchTopInterfaces(1440, 20);
|
||||
if (ifaces?.length) { db.insertInterfaces(ifaces, fetchedAt); console.log(`[Scheduler] OK Interfaces : ${ifaces.length} baris`); }
|
||||
else console.log(`[Scheduler] -- Interfaces : tidak ada data`);
|
||||
|
||||
// 16. Flow Types
|
||||
const flowTypes = await netify.fetchTopFlowTypes(1440, 10);
|
||||
if (flowTypes?.length) { db.insertFlowTypes(flowTypes, fetchedAt); console.log(`[Scheduler] OK FlowTypes : ${flowTypes.length} baris`); }
|
||||
else console.log(`[Scheduler] -- FlowTypes : tidak ada data`);
|
||||
|
||||
// 17. Flow Origins
|
||||
const flowOrigins = await netify.fetchTopFlowOrigins(1440, 10);
|
||||
if (flowOrigins?.length) { db.insertFlowOrigins(flowOrigins, fetchedAt); console.log(`[Scheduler] OK FlowOrigin : ${flowOrigins.length} baris`); }
|
||||
else console.log(`[Scheduler] -- FlowOrigin : tidak ada data`);
|
||||
|
||||
// 18. IP Versions
|
||||
const ipVersions = await netify.fetchTopIPVersions(1440, 5);
|
||||
if (ipVersions?.length) { db.insertIPVersions(ipVersions, fetchedAt); console.log(`[Scheduler] OK IPVersions : ${ipVersions.length} baris`); }
|
||||
else console.log(`[Scheduler] -- IPVersions : tidak ada data`);
|
||||
|
||||
// 19. Remote IPs
|
||||
const remoteIPs = await netify.fetchTopRemoteIPs(1440, 20);
|
||||
if (remoteIPs?.length) { db.insertRemoteIPs(remoteIPs, fetchedAt); console.log(`[Scheduler] OK RemoteIPs : ${remoteIPs.length} baris`); }
|
||||
else console.log(`[Scheduler] -- RemoteIPs : tidak ada data`);
|
||||
|
||||
// 20. MAC Bandwidth
|
||||
const macBW = await netify.fetchTopLocalMACs(1440, 50);
|
||||
if (macBW?.length) { db.insertMACBandwidth(macBW, fetchedAt); console.log(`[Scheduler] OK MACBandwdh : ${macBW.length} baris`); }
|
||||
else console.log(`[Scheduler] -- MACBandwdh : tidak ada data`);
|
||||
|
||||
// 9. Bandwidth Timeline
|
||||
const summary = await netify.fetchBandwidthSummary(1440);
|
||||
const devCount = devices?.length ?? 0;
|
||||
db.insertBandwidthTimeline({ ...summary, devices: devCount }, fetchedAt);
|
||||
console.log(`[Scheduler] OK Timeline : saved`);
|
||||
|
||||
// 21. TLS Versions
|
||||
const tlsVer = await netify.fetchTLSVersions(1440, 10);
|
||||
if (tlsVer?.length) { db.insertTLSVersions(tlsVer, fetchedAt); console.log(`[Scheduler] OK TLS Ver : ${tlsVer.length} baris`); }
|
||||
else console.log(`[Scheduler] -- TLS Ver : tidak ada data`);
|
||||
|
||||
// 22. TLS Ciphers
|
||||
const tlsCipher = await netify.fetchTLSCiphers(1440, 15);
|
||||
if (tlsCipher?.length) { db.insertTLSCiphers(tlsCipher, fetchedAt); console.log(`[Scheduler] OK TLS Cipher : ${tlsCipher.length} baris`); }
|
||||
else console.log(`[Scheduler] -- TLS Cipher : tidak ada data`);
|
||||
|
||||
// 23. TLS Security
|
||||
const tlsSec = await netify.fetchTLSSecurity(1440, 10);
|
||||
if (tlsSec?.length) { db.insertTLSSecurity(tlsSec, fetchedAt); console.log(`[Scheduler] OK TLS Sec : ${tlsSec.length} baris`); }
|
||||
else console.log(`[Scheduler] -- TLS Sec : tidak ada data`);
|
||||
|
||||
// 24. NetBIOS Hostnames
|
||||
const netbios = await netify.fetchNetBIOSHostnames(1440, 30);
|
||||
if (netbios?.length) { db.insertNetBIOSHostnames(netbios, fetchedAt); console.log(`[Scheduler] OK NetBIOS : ${netbios.length} baris`); }
|
||||
else console.log(`[Scheduler] -- NetBIOS : tidak ada data`);
|
||||
|
||||
// 25. Discovery OS (standalone — OS yang terdeteksi di jaringan)
|
||||
const discOs = await netify.fetchTopDiscoveryOS(1440, 20);
|
||||
if (discOs?.length) { db.insertDiscoveryOS(discOs, fetchedAt); console.log(`[Scheduler] OK DiscOS : ${discOs.length} baris`); }
|
||||
else console.log(`[Scheduler] -- DiscOS : tidak ada data`);
|
||||
|
||||
// 26. DHCP Class Fingerprint
|
||||
const dhcpFp = await netify.fetchDHCPClassFingerprints(1440, 30);
|
||||
if (dhcpFp?.length) { db.insertDHCPFingerprints(dhcpFp, fetchedAt); console.log(`[Scheduler] OK DHCP FP : ${dhcpFp.length} baris`); }
|
||||
else console.log(`[Scheduler] -- DHCP FP : tidak ada data`);
|
||||
|
||||
// 27. HTTP User-Agent
|
||||
const userAgents = await netify.fetchHTTPUserAgents(1440, 30);
|
||||
if (userAgents?.length) { db.insertHTTPUserAgents(userAgents, fetchedAt); console.log(`[Scheduler] OK UserAgent : ${userAgents.length} baris`); }
|
||||
else console.log(`[Scheduler] -- UserAgent : tidak ada data`);
|
||||
|
||||
// 28. HTTPS SNI Hostname
|
||||
const sniHosts = await netify.fetchSNIHostnames(1440, 30);
|
||||
if (sniHosts?.length) { db.insertSNIHostnames(sniHosts, fetchedAt); console.log(`[Scheduler] OK SNI Host : ${sniHosts.length} baris`); }
|
||||
else console.log(`[Scheduler] -- SNI Host : tidak ada data`);
|
||||
|
||||
// 29. SSL Server Common Name
|
||||
const sslCN = await netify.fetchSSLServerCN(1440, 30);
|
||||
if (sslCN?.length) { db.insertSSLServerCN(sslCN, fetchedAt); console.log(`[Scheduler] OK SSL CN : ${sslCN.length} baris`); }
|
||||
else console.log(`[Scheduler] -- SSL CN : tidak ada data`);
|
||||
|
||||
// 30. QUIC Hostname
|
||||
const quicHosts = await netify.fetchQUICHostnames(1440, 30);
|
||||
if (quicHosts?.length) { db.insertQUICHostnames(quicHosts, fetchedAt); console.log(`[Scheduler] OK QUIC Host : ${quicHosts.length} baris`); }
|
||||
else console.log(`[Scheduler] -- QUIC Host : tidak ada data`);
|
||||
|
||||
// 31. BitTorrent Info Hash
|
||||
const btHashes = await netify.fetchBitTorrentInfoHashes(1440, 30);
|
||||
if (btHashes?.length) { db.insertBitTorrentHashes(btHashes, fetchedAt); console.log(`[Scheduler] OK BT Hash : ${btHashes.length} baris`); }
|
||||
else console.log(`[Scheduler] -- BT Hash : tidak ada data`);
|
||||
|
||||
// 32. SSH Client (field: ssh_client)
|
||||
const sshClient = await netify.fetchSSHClients(1440, 20);
|
||||
if (sshClient?.length) { db.insertSSHVersions(sshClient, fetchedAt); console.log(`[Scheduler] OK SSH Client : ${sshClient.length} baris`); }
|
||||
else console.log(`[Scheduler] -- SSH Client : tidak ada data`);
|
||||
|
||||
// 32b. SSH Server (field: ssh_server)
|
||||
const sshServer = await netify.fetchSSHServers(1440, 20);
|
||||
if (sshServer?.length) { db.insertSSHVersions(sshServer, fetchedAt); console.log(`[Scheduler] OK SSH Server : ${sshServer.length} baris`); }
|
||||
else console.log(`[Scheduler] -- SSH Server : tidak ada data`);
|
||||
|
||||
// 33. mDNS Hostname (Chromecast, Apple TV, etc.)
|
||||
const mdnsHosts = await netify.fetchMDNSHostnames(1440, 30);
|
||||
if (mdnsHosts?.length) { db.insertMDNSHostnames(mdnsHosts, fetchedAt); console.log(`[Scheduler] OK mDNS Host : ${mdnsHosts.length} baris`); }
|
||||
else console.log(`[Scheduler] -- mDNS Host : tidak ada data`);
|
||||
|
||||
// ─── INTELLIGENCE 22-30 (derive dari data yang tersedia) ─────────────────
|
||||
|
||||
// 34. Cryptocurrency Mining (derive dari apps + flows ke port mining)
|
||||
const cryptoMining = await netify.fetchCryptoMining(50);
|
||||
if (cryptoMining?.length) { db.insertCryptoMining(cryptoMining, fetchedAt); console.log(`[Scheduler] OK CryptoMine : ${cryptoMining.length} baris`); }
|
||||
else console.log(`[Scheduler] -- CryptoMine : tidak ada data`);
|
||||
|
||||
// 35. Device Discovery (derive dari flows + bandwidth per-IP)
|
||||
const devDisc = await netify.fetchDeviceDiscovery(100);
|
||||
if (devDisc?.length) { db.insertDeviceDiscovery(devDisc, fetchedAt); console.log(`[Scheduler] OK DevDisc : ${devDisc.length} baris`); }
|
||||
else console.log(`[Scheduler] -- DevDisc : tidak ada data`);
|
||||
|
||||
// 36. Encryption Audit (derive dari flows per-IP: port encrypted vs plain)
|
||||
const encAudit = await netify.fetchEncryptionAudit(50);
|
||||
if (encAudit?.length) { db.insertEncryptionAudit(encAudit, fetchedAt); console.log(`[Scheduler] OK EncAudit : ${encAudit.length} baris`); }
|
||||
else console.log(`[Scheduler] -- EncAudit : tidak ada data`);
|
||||
|
||||
// 37. Insecure Protocols (derive dari top protocols)
|
||||
const insecProto = await netify.fetchInsecureProtocols(1440, 50);
|
||||
if (insecProto?.length) { db.insertInsecureProtocols(insecProto, fetchedAt); console.log(`[Scheduler] OK InsecProto : ${insecProto.length} baris`); }
|
||||
else console.log(`[Scheduler] -- InsecProto : tidak ada data`);
|
||||
|
||||
// 38. IP Reputation (derive dari top remote_ip + high-risk countries)
|
||||
const ipRep = await netify.fetchIPReputation(50);
|
||||
if (ipRep?.length) { db.insertIPReputation(ipRep, fetchedAt); console.log(`[Scheduler] OK IPRepute : ${ipRep.length} baris`); }
|
||||
else console.log(`[Scheduler] -- IPRepute : tidak ada data`);
|
||||
|
||||
// 39. Server Discovery (derive dari flows ke port server well-known)
|
||||
const srvDisc = await netify.fetchServerDiscovery(100);
|
||||
if (srvDisc?.length) { db.insertServerDiscovery(srvDisc, fetchedAt); console.log(`[Scheduler] OK SrvDisc : ${srvDisc.length} baris`); }
|
||||
else console.log(`[Scheduler] -- SrvDisc : tidak ada data`);
|
||||
|
||||
// 40. Tor Detection (derive dari apps/hostnames mengandung "tor")
|
||||
const torDet = await netify.fetchTorDetection(50);
|
||||
if (torDet?.length) { db.insertTorDetection(torDet, fetchedAt); console.log(`[Scheduler] OK TorDet : ${torDet.length} baris`); }
|
||||
else console.log(`[Scheduler] -- TorDet : tidak ada data`);
|
||||
|
||||
// 41. Unencrypted Password (derive dari flows ke port cleartext auth)
|
||||
const unencPwd = await netify.fetchUnencryptedPasswords(50);
|
||||
if (unencPwd?.length) { db.insertUnencryptedPasswords(unencPwd, fetchedAt); console.log(`[Scheduler] OK UnencPwd : ${unencPwd.length} baris`); }
|
||||
else console.log(`[Scheduler] -- UnencPwd : tidak ada data`);
|
||||
|
||||
// 42. VPN Detection (derive dari apps/protocols/ports VPN)
|
||||
const vpnDet = await netify.fetchVPNDetection(50);
|
||||
if (vpnDet?.length) { db.insertVPNDetection(vpnDet, fetchedAt); console.log(`[Scheduler] OK VPNDet : ${vpnDet.length} baris`); }
|
||||
else console.log(`[Scheduler] -- VPNDet : tidak ada data`);
|
||||
|
||||
} catch (err) {
|
||||
console.error('[Scheduler] ERROR:', err);
|
||||
} finally {
|
||||
isRunning = false;
|
||||
console.log(`[Scheduler] Poll selesai.\n`);
|
||||
}
|
||||
}
|
||||
|
||||
function startScheduler() {
|
||||
runPoll();
|
||||
cron.schedule('* * * * *', () => runPoll());
|
||||
console.log('[Scheduler] Aktif. Polling setiap 1 menit.\n');
|
||||
}
|
||||
|
||||
module.exports = { startScheduler, runPoll };
|
||||
@@ -0,0 +1,32 @@
|
||||
// backend/server.js
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') });
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.BACKEND_PORT || 3001;
|
||||
|
||||
// ── Middleware ────────────────────────────────────────────────────────────────
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// ── API Routes ────────────────────────────────────────────────────────────────
|
||||
const dashboardRoutes = require('./routes/dashboard');
|
||||
app.use('/api/dashboard', dashboardRoutes);
|
||||
|
||||
// ── Health Check ──────────────────────────────────────────────────────────────
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ ok: true, message: 'Netify Dashboard Backend berjalan!', time: new Date().toISOString() });
|
||||
});
|
||||
|
||||
|
||||
// ── Start Server ──────────────────────────────────────────────────────────────
|
||||
app.listen(PORT, () => {
|
||||
console.log(`\n🚀 API Server berjalan di http://localhost:${PORT}`);
|
||||
console.log(`🔌 API Health : http://localhost:${PORT}/api/health\n`);
|
||||
|
||||
// Mulai scheduler polling
|
||||
const { startScheduler } = require('./scheduler');
|
||||
startScheduler();
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const axios = require('axios');
|
||||
|
||||
// Save the original axios get method
|
||||
const originalGet = axios.get;
|
||||
|
||||
// Helper to mock axios.get response
|
||||
function mockAxiosGet(mockFn) {
|
||||
axios.get = mockFn;
|
||||
}
|
||||
|
||||
// Restore original axios get
|
||||
function restoreAxiosGet() {
|
||||
axios.get = originalGet;
|
||||
}
|
||||
|
||||
test.afterEach(() => {
|
||||
restoreAxiosGet();
|
||||
});
|
||||
|
||||
test('fetchEvents parses raw events successfully', async () => {
|
||||
const netify = require('../netify');
|
||||
|
||||
// Mock API response
|
||||
mockAxiosGet(async (url, config) => {
|
||||
if (url.endsWith('/event/events')) {
|
||||
return {
|
||||
data: {
|
||||
status_code: 0,
|
||||
status_message: 'Success.',
|
||||
data: [
|
||||
{
|
||||
id: 254,
|
||||
basename: 'new.device',
|
||||
severity: 10,
|
||||
label: 'New device detected',
|
||||
description: JSON.stringify({
|
||||
default: 'New device {{ device_label }} discovered',
|
||||
tags: {
|
||||
device_label: 'Linux Device',
|
||||
device_ip: '192.168.1.100'
|
||||
}
|
||||
}),
|
||||
created_at: {
|
||||
date: '2026-06-29 10:46:20'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected mock URL: ${url}`);
|
||||
});
|
||||
|
||||
const parsed = await netify.fetchEvents(50);
|
||||
assert.ok(Array.isArray(parsed));
|
||||
assert.strictEqual(parsed.length, 1);
|
||||
|
||||
const event = parsed[0];
|
||||
assert.strictEqual(event.event_id, 254);
|
||||
assert.strictEqual(event.event_type, 'new.device');
|
||||
assert.strictEqual(event.severity, 'Warning');
|
||||
assert.strictEqual(event.description, 'New device Linux Device discovered');
|
||||
assert.strictEqual(event.ip_address, '192.168.1.100');
|
||||
assert.strictEqual(event.event_at, '2026-06-29 10:46:20');
|
||||
});
|
||||
|
||||
test('fetchDiscoveredDevices merges devices and flows successfully', async () => {
|
||||
const netify = require('../netify');
|
||||
|
||||
mockAxiosGet(async (url, config) => {
|
||||
if (url.endsWith('/intelligence/discovery/devices')) {
|
||||
return {
|
||||
data: {
|
||||
status_code: 0,
|
||||
data: [
|
||||
{
|
||||
ip: { address: '10.250.192.226' },
|
||||
mac_address: 'b2:87:41:96:a7:7b',
|
||||
mac_vendor: 'Local',
|
||||
discovery_mac: {
|
||||
address: 'b2:87:41:96:a7:7b',
|
||||
discovery_hardware: 'Linux Device'
|
||||
},
|
||||
discovery_type: { label: 'Computer' },
|
||||
discovery_os: { label: 'Linux' }
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
if (url.endsWith('/data/stats/top/local_ip/download')) {
|
||||
return {
|
||||
data: {
|
||||
status_code: 0,
|
||||
data: [
|
||||
{ local_ip: { address: '10.250.192.226' }, download: 1000000 }
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
if (url.endsWith('/data/stats/top/local_ip/upload')) {
|
||||
return {
|
||||
data: {
|
||||
status_code: 0,
|
||||
data: [
|
||||
{ local_ip: { address: '10.250.192.226' }, upload: 500000 }
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected mock URL: ${url}`);
|
||||
});
|
||||
|
||||
const devices = await netify.fetchDiscoveredDevices(1440, 50);
|
||||
assert.ok(Array.isArray(devices));
|
||||
assert.strictEqual(devices.length, 1);
|
||||
|
||||
const dev = devices[0];
|
||||
assert.strictEqual(dev.ip_address, '10.250.192.226');
|
||||
assert.strictEqual(dev.mac_address, 'b2:87:41:96:a7:7b');
|
||||
assert.strictEqual(dev.download, 1000000);
|
||||
assert.strictEqual(dev.upload, 500000);
|
||||
});
|
||||
+8
-1
@@ -1,7 +1,14 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/api/:path*',
|
||||
destination: `${process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:3001'}/api/:path*`,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
Generated
+2059
-27
File diff suppressed because it is too large.
Load diff
+20
-3
@@ -3,21 +3,38 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "concurrently \"next dev\" \"node backend/server.js\"",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
"lint": "eslint",
|
||||
"backend": "node backend/server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.18.1",
|
||||
"better-sqlite3": "^12.11.1",
|
||||
"clsx": "^2.1.1",
|
||||
"concurrently": "^10.0.3",
|
||||
"cors": "^2.8.6",
|
||||
"d3-scale": "^4.0.2",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
"lucide-react": "^1.21.0",
|
||||
"next": "16.2.9",
|
||||
"node-cron": "^4.5.0",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
"react-dom": "19.2.4",
|
||||
"react-simple-maps": "^3.0.0",
|
||||
"recharts": "^3.9.0",
|
||||
"swr": "^2.4.2",
|
||||
"tailwind-merge": "^3.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/d3-scale": "^4.0.9",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/react-simple-maps": "^3.0.6",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.9",
|
||||
"tailwindcss": "^4",
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
const fs = require('fs');
|
||||
const files = [
|
||||
'src/app/network-infrastructure/page.tsx',
|
||||
'src/app/dpi-analytics/page.tsx',
|
||||
'src/app/intelligence/page.tsx'
|
||||
];
|
||||
|
||||
files.forEach(f => {
|
||||
let text = fs.readFileSync(f, 'utf8');
|
||||
text = text.replace(/accessorKey:\s*"([^"]+)"/g, 'accessor: (row: any) => row.$1');
|
||||
text = text.replace(/accessorKey:\s*\(/g, 'accessor: (');
|
||||
fs.writeFileSync(f, text);
|
||||
});
|
||||
console.log('Fixed accessorKey');
|
||||
@@ -0,0 +1,35 @@
|
||||
const fs = require('fs');
|
||||
const fetch = global.fetch;
|
||||
|
||||
const env = fs.readFileSync('.env.local', 'utf8').split('\n').reduce((acc, line) => {
|
||||
const match = line.match(/^([^#\s=]+)=(.*)$/);
|
||||
if (match) {
|
||||
acc[match[1]] = match[2];
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
async function test() {
|
||||
try {
|
||||
const API_URL = env.NETIFY_BASE_URL ? env.NETIFY_BASE_URL + '/assets/agents' : 'https://manager.netify.ai/api/v1/assets/agents';
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
|
||||
if (env.NETIFY_JWT_TOKEN && env.NETIFY_SITE_UUID) {
|
||||
headers['x-api-key'] = env.NETIFY_JWT_TOKEN;
|
||||
headers['x-net-site'] = env.NETIFY_SITE_UUID;
|
||||
} else if (env.NETIFY_API_KEY) {
|
||||
headers['x-api-key'] = env.NETIFY_API_KEY;
|
||||
}
|
||||
|
||||
console.log('Fetching', API_URL);
|
||||
console.log('Headers:', Object.keys(headers));
|
||||
|
||||
const res = await fetch(API_URL, { headers });
|
||||
console.log('Status:', res.status, res.statusText);
|
||||
const data = await res.text();
|
||||
console.log('Data:', data.substring(0, 500));
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
test();
|
||||
@@ -0,0 +1,250 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { getAgents, createAgent, updateAgent, deleteAgent, Agent } from "@/lib/actions/agents";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
||||
import { DataTable, Column } from "@/components/ui/DataTable";
|
||||
import { Modal } from "@/components/ui/Modal";
|
||||
import { Badge } from "@/components/ui/Badge";
|
||||
import { Loader2, Plus, Edit, Trash2, Server, CheckCircle2, XCircle } from "lucide-react";
|
||||
|
||||
export default function AgentsPage() {
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
// Modals state
|
||||
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
||||
const [isEditOpen, setIsEditOpen] = useState(false);
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||
|
||||
// Form state
|
||||
const [selectedAgent, setSelectedAgent] = useState<Agent | null>(null);
|
||||
const [formData, setFormData] = useState({ agentId: "", label: "" });
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const loadAgents = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await getAgents();
|
||||
setAgents(data);
|
||||
} catch (e: any) {
|
||||
setError(e.message);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadAgents();
|
||||
}, []);
|
||||
|
||||
const openCreate = () => {
|
||||
setFormData({ agentId: "", label: "" });
|
||||
setIsCreateOpen(true);
|
||||
};
|
||||
|
||||
const openEdit = (agent: Agent) => {
|
||||
setSelectedAgent(agent);
|
||||
setFormData({ agentId: agent.uuid || agent.serial, label: agent.label || "" });
|
||||
setIsEditOpen(true);
|
||||
};
|
||||
|
||||
const openDelete = (agent: Agent) => {
|
||||
setSelectedAgent(agent);
|
||||
setIsDeleteOpen(true);
|
||||
};
|
||||
|
||||
const handleCreate = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
const res = await createAgent(formData.agentId, formData.label);
|
||||
setIsSubmitting(false);
|
||||
|
||||
if (res.success) {
|
||||
setIsCreateOpen(false);
|
||||
loadAgents();
|
||||
} else {
|
||||
alert(res.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!selectedAgent) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
const res = await updateAgent(selectedAgent.uuid || selectedAgent.serial, formData.label);
|
||||
setIsSubmitting(false);
|
||||
|
||||
if (res.success) {
|
||||
setIsEditOpen(false);
|
||||
loadAgents();
|
||||
} else {
|
||||
alert(res.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!selectedAgent) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
const res = await deleteAgent(selectedAgent.uuid || selectedAgent.serial);
|
||||
setIsSubmitting(false);
|
||||
|
||||
if (res.success) {
|
||||
setIsDeleteOpen(false);
|
||||
loadAgents();
|
||||
} else {
|
||||
alert(res.message);
|
||||
}
|
||||
};
|
||||
|
||||
const columns: Column<Agent>[] = [
|
||||
{ header: "UUID / Serial", accessor: (row) => <span className="font-mono text-xs text-blue-400">{row.uuid || row.serial}</span> },
|
||||
{ header: "Label", accessor: (row) => row.label || <span className="text-muted-foreground italic">No label</span> },
|
||||
{ header: "Provisioned", accessor: (row) => row.provisioned ? <Badge variant="green"><CheckCircle2 className="w-3 h-3 mr-1" /> Yes</Badge> : <Badge variant="red"><XCircle className="w-3 h-3 mr-1" /> No</Badge> },
|
||||
{ header: "Activated", accessor: (row) => row.activated ? <Badge variant="green"><CheckCircle2 className="w-3 h-3 mr-1" /> Yes</Badge> : <Badge variant="red"><XCircle className="w-3 h-3 mr-1" /> No</Badge> },
|
||||
{ header: "Last Seen", accessor: (row) => <span className="text-muted-foreground">{row.last_seen_at?.human || '-'}</span> },
|
||||
{
|
||||
header: "Actions",
|
||||
accessor: (row) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={() => openEdit(row)} className="p-1.5 text-slate-400 hover:text-white hover:bg-slate-800 rounded transition-colors">
|
||||
<Edit className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={() => openDelete(row)} className="p-1.5 text-red-400 hover:text-red-300 hover:bg-red-950/30 rounded transition-colors">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold tracking-tight text-white flex items-center gap-3">
|
||||
<Server className="w-6 h-6 text-primary" />
|
||||
Agents Inventory
|
||||
{isLoading && <Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />}
|
||||
</h2>
|
||||
<button
|
||||
onClick={openCreate}
|
||||
className="flex items-center gap-2 bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded-md text-sm font-medium transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Provision Agent
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Active Agents {agents.length > 0 ? `(${agents.length})` : ''}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{error ? (
|
||||
<div className="p-4 bg-red-950/30 border border-red-900/50 rounded-lg text-red-400 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
) : (
|
||||
<DataTable
|
||||
data={agents}
|
||||
columns={columns}
|
||||
searchPlaceholder="Search agents by UUID or label..."
|
||||
searchFilter={(row, query) => {
|
||||
const q = query.toLowerCase();
|
||||
return (row.uuid || "").toLowerCase().includes(q) ||
|
||||
(row.serial || "").toLowerCase().includes(q) ||
|
||||
(row.label || "").toLowerCase().includes(q);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Create Modal */}
|
||||
<Modal isOpen={isCreateOpen} onClose={() => setIsCreateOpen(false)} title="Provision New Agent">
|
||||
<form onSubmit={handleCreate} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-300">Agent UUID / Serial *</label>
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
placeholder="e.g. EX-AM-PL-ES"
|
||||
value={formData.agentId}
|
||||
onChange={e => setFormData({...formData, agentId: e.target.value})}
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-md px-3 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-300">Agent Label</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. Branch Office Gateway"
|
||||
value={formData.label}
|
||||
onChange={e => setFormData({...formData, label: e.target.value})}
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-md px-3 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-4 flex justify-end gap-3">
|
||||
<button type="button" onClick={() => setIsCreateOpen(false)} className="px-4 py-2 text-sm font-medium text-slate-300 hover:text-white transition-colors">Cancel</button>
|
||||
<button type="submit" disabled={isSubmitting} className="flex items-center gap-2 bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded-md text-sm font-medium transition-colors disabled:opacity-50">
|
||||
{isSubmitting ? <Loader2 className="w-4 h-4 animate-spin" /> : null}
|
||||
Create Agent
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
{/* Edit Modal */}
|
||||
<Modal isOpen={isEditOpen} onClose={() => setIsEditOpen(false)} title="Edit Agent">
|
||||
<form onSubmit={handleUpdate} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-400">Agent UUID / Serial</label>
|
||||
<input
|
||||
disabled
|
||||
type="text"
|
||||
value={selectedAgent?.uuid || selectedAgent?.serial || ""}
|
||||
className="w-full bg-slate-900/50 border border-slate-800 rounded-md px-3 py-2 text-sm text-slate-500 cursor-not-allowed"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-300">Agent Label</label>
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder="e.g. Branch Office Gateway"
|
||||
value={formData.label}
|
||||
onChange={e => setFormData({...formData, label: e.target.value})}
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-md px-3 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-4 flex justify-end gap-3">
|
||||
<button type="button" onClick={() => setIsEditOpen(false)} className="px-4 py-2 text-sm font-medium text-slate-300 hover:text-white transition-colors">Cancel</button>
|
||||
<button type="submit" disabled={isSubmitting} className="flex items-center gap-2 bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded-md text-sm font-medium transition-colors disabled:opacity-50">
|
||||
{isSubmitting ? <Loader2 className="w-4 h-4 animate-spin" /> : null}
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
{/* Delete Modal */}
|
||||
<Modal isOpen={isDeleteOpen} onClose={() => setIsDeleteOpen(false)} title="Confirm Deletion">
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-slate-300">
|
||||
Are you sure you want to delete the agent <span className="font-bold text-white">{selectedAgent?.label || selectedAgent?.uuid}</span>? This action cannot be undone.
|
||||
</p>
|
||||
<div className="pt-4 flex justify-end gap-3">
|
||||
<button type="button" onClick={() => setIsDeleteOpen(false)} className="px-4 py-2 text-sm font-medium text-slate-300 hover:text-white transition-colors">Cancel</button>
|
||||
<button onClick={handleDelete} disabled={isSubmitting} className="flex items-center gap-2 bg-destructive hover:bg-destructive/90 text-destructive-foreground px-4 py-2 rounded-md text-sm font-medium transition-colors disabled:opacity-50">
|
||||
{isSubmitting ? <Loader2 className="w-4 h-4 animate-spin" /> : null}
|
||||
Delete Agent
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(req: NextRequest, props: { params: Promise<{ route: string[] }> }) {
|
||||
return handleProxy(req, props);
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest, props: { params: Promise<{ route: string[] }> }) {
|
||||
return handleProxy(req, props);
|
||||
}
|
||||
|
||||
export async function PUT(req: NextRequest, props: { params: Promise<{ route: string[] }> }) {
|
||||
return handleProxy(req, props);
|
||||
}
|
||||
|
||||
export async function DELETE(req: NextRequest, props: { params: Promise<{ route: string[] }> }) {
|
||||
return handleProxy(req, props);
|
||||
}
|
||||
|
||||
export async function PATCH(req: NextRequest, props: { params: Promise<{ route: string[] }> }) {
|
||||
return handleProxy(req, props);
|
||||
}
|
||||
|
||||
async function handleProxy(req: NextRequest, { params }: { params: Promise<{ route: string[] }> }) {
|
||||
try {
|
||||
const route = (await params).route.join('/');
|
||||
const baseUrl = process.env.NETIFY_BASE_URL || 'https://manager.netify.ai/api/v1';
|
||||
|
||||
// Construct the destination URL including search parameters
|
||||
const url = new URL(req.url);
|
||||
const destinationUrl = `${baseUrl}/${route}${url.search}`;
|
||||
|
||||
// Prepare headers
|
||||
const headers = new Headers();
|
||||
headers.set('Content-Type', 'application/json');
|
||||
|
||||
if (process.env.NETIFY_JWT_TOKEN && process.env.NETIFY_SITE_UUID) {
|
||||
headers.set('x-api-key', process.env.NETIFY_JWT_TOKEN);
|
||||
headers.set('x-net-site', process.env.NETIFY_SITE_UUID);
|
||||
} else if (process.env.NETIFY_API_KEY) {
|
||||
headers.set('x-api-key', process.env.NETIFY_API_KEY);
|
||||
}
|
||||
|
||||
// Some requests like GET cannot have a body
|
||||
const hasBody = !['GET', 'HEAD'].includes(req.method);
|
||||
let body = null;
|
||||
|
||||
if (hasBody) {
|
||||
body = await req.text();
|
||||
}
|
||||
|
||||
const response = await fetch(destinationUrl, {
|
||||
method: req.method,
|
||||
headers,
|
||||
body: body || undefined,
|
||||
});
|
||||
|
||||
// Return raw text so we don't try to parse it if it's not JSON
|
||||
const responseText = await response.text();
|
||||
|
||||
return new NextResponse(responseText, {
|
||||
status: response.status,
|
||||
headers: {
|
||||
'Content-Type': response.headers.get('content-type') || 'application/json',
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[Proxy Error]', error);
|
||||
return NextResponse.json({ ok: false, message: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTopApps } from "@/lib/api";
|
||||
import { DataTable, Column } from "@/components/ui/DataTable";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { fmtBytes } from "@/lib/utils";
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer } from "recharts";
|
||||
|
||||
export default function AppsPage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { data, isLoading } = useTopApps(50); // fetch 50 apps
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
const top5 = data.slice(0, 5);
|
||||
|
||||
const columns: Column<typeof data[0]>[] = [
|
||||
{ header: "#", accessor: (row, i) => i + 1 },
|
||||
{
|
||||
header: "Application",
|
||||
accessor: (row) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{row.favicon && (
|
||||
<img src={row.favicon} alt={row.app_label} className="w-5 h-5 rounded-sm bg-white p-0.5" />
|
||||
)}
|
||||
<span>{row.app_label}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{ header: "Category", accessor: (row) => row.category || "-" },
|
||||
{
|
||||
header: "Download",
|
||||
accessor: (row) => <span className="text-blue-400">{fmtBytes(row.download)}</span>
|
||||
},
|
||||
{
|
||||
header: "Upload",
|
||||
accessor: (row) => <span className="text-green-500">{fmtBytes(row.upload)}</span>
|
||||
},
|
||||
{
|
||||
header: "Total",
|
||||
accessor: (row) => <span className="font-medium text-white">{fmtBytes(row.download + row.upload)}</span>
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold tracking-tight text-white flex items-center gap-3">
|
||||
Applications
|
||||
{isLoading && <Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Top 5 Applications by Volume</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-[250px] w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={top5} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#30363d" vertical={false} />
|
||||
<XAxis dataKey="app_label" stroke="#8b949e" tickLine={false} axisLine={false} />
|
||||
<YAxis
|
||||
stroke="#8b949e"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => {
|
||||
const gb = value / 1073741824;
|
||||
const mb = value / 1048576;
|
||||
return gb >= 1 ? `${gb.toFixed(1)}GB` : `${mb.toFixed(0)}MB`;
|
||||
}}
|
||||
/>
|
||||
<RechartsTooltip
|
||||
cursor={{fill: '#21262d'}}
|
||||
contentStyle={{ backgroundColor: '#161b22', borderColor: '#30363d', color: '#c9d1d9' }}
|
||||
formatter={(value: any) => [fmtBytes(value as number), 'Volume']}
|
||||
/>
|
||||
<Bar dataKey="download" name="Download" fill="#58a6ff" radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey="upload" name="Upload" fill="#3fb950" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">All Applications ({data.length})</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
searchPlaceholder="Search by application or category..."
|
||||
searchFilter={(row, query) => {
|
||||
const q = query.toLowerCase();
|
||||
return (
|
||||
(row.app_label || "").toLowerCase().includes(q) ||
|
||||
(row.category || "").toLowerCase().includes(q)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
||||
import { DataTable, Column } from "@/components/ui/DataTable";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useDevices, DeviceStat } from "@/lib/api";
|
||||
import { fmtBytes } from "@/lib/utils";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
export default function DevicesPage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const devices = useDevices(100);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
const columns: Column<DeviceStat>[] = [
|
||||
{ header: "#", accessor: (row, i) => i + 1 },
|
||||
{ header: "IP Address", accessor: (row) => <span className="font-medium text-blue-400">{row.ip_address || '-'}</span> },
|
||||
{ header: "MAC Address", accessor: (row) => row.mac_address ? <code className="text-xs bg-secondary/50 px-1 py-0.5 rounded text-muted-foreground">{row.mac_address}</code> : '-' },
|
||||
{ header: "Type", accessor: (row) => row.device_type || '-' },
|
||||
{ header: "OS", accessor: (row) => row.os_label || '-' },
|
||||
{ header: "Manufacturer", accessor: (row) => row.manufacturer || '-' },
|
||||
{ header: "Download", accessor: (row) => <span className="text-cyan-400">{fmtBytes(row.download)}</span>, className: "text-right" },
|
||||
{ header: "Upload", accessor: (row) => <span className="text-green-400">{fmtBytes(row.upload)}</span>, className: "text-right" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-in pb-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight text-white flex items-center gap-3">
|
||||
Devices
|
||||
{devices.isLoading && <Loader2 className="w-5 h-5 animate-spin text-primary" />}
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-1">Monitor and manage connected network devices.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="bg-card/80 backdrop-blur-xl border-border/50 animate-slide-up" style={{ animationDelay: '100ms' }}>
|
||||
<CardHeader className="border-b border-border/50 pb-4 mb-4">
|
||||
<CardTitle className="text-lg text-white">Active Devices {devices.data ? `(${devices.data.length})` : ''}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={devices.data || []}
|
||||
columns={columns}
|
||||
searchPlaceholder="Search devices by IP, MAC, OS..."
|
||||
searchFilter={(row, query) => {
|
||||
const q = query.toLowerCase();
|
||||
return (row.ip_address || "").toLowerCase().includes(q) ||
|
||||
(row.mac_address || "").toLowerCase().includes(q) ||
|
||||
(row.os_label || "").toLowerCase().includes(q) ||
|
||||
(row.device_type || "").toLowerCase().includes(q);
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useDNS } from "@/lib/api";
|
||||
import { DataTable, Column } from "@/components/ui/DataTable";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
export default function DNSPage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { data, isLoading } = useDNS(100); // fetch 100 DNS queries
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
const columns: Column<typeof data[0]>[] = [
|
||||
{ header: "#", accessor: (row, i) => i + 1 },
|
||||
{
|
||||
header: "Domain",
|
||||
accessor: (row) => <span className="font-medium text-white">{row.domain}</span>
|
||||
},
|
||||
{ header: "Application", accessor: (row) => row.app_label || "-" },
|
||||
{ header: "Category", accessor: (row) => row.category || "-" },
|
||||
{
|
||||
header: "Query Count",
|
||||
accessor: (row) => <span className="text-purple-400">{row.query_count.toLocaleString()}</span>
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold tracking-tight text-white flex items-center gap-3">
|
||||
DNS Intelligence
|
||||
{isLoading && <Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Top DNS Queries ({data.length})</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
searchPlaceholder="Search by domain, application, or category..."
|
||||
searchFilter={(row, query) => {
|
||||
const q = query.toLowerCase();
|
||||
return (
|
||||
(row.domain || "").toLowerCase().includes(q) ||
|
||||
(row.app_label || "").toLowerCase().includes(q) ||
|
||||
(row.category || "").toLowerCase().includes(q)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Tabs, Tab } from "@/components/ui/Tabs";
|
||||
import { DataTable, Column } from "@/components/ui/DataTable";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
||||
import {
|
||||
useNetbios, useDiscoveryOs, useDhcpFingerprints,
|
||||
useHttpUserAgents, useSniHostnames, useSslServerCn,
|
||||
useQuicHostnames, useBittorrentHashes, useSshVersions,
|
||||
useMdnsHostnames
|
||||
} from "@/lib/api-advanced";
|
||||
|
||||
export default function DpiAnalyticsPage() {
|
||||
// Hostnames & Servers Tab
|
||||
const { data: sniHostnames, isLoading: loadingSni } = useSniHostnames(20);
|
||||
const { data: sslServerCn, isLoading: loadingSslCn } = useSslServerCn(20);
|
||||
const { data: quicHostnames, isLoading: loadingQuic } = useQuicHostnames(20);
|
||||
const { data: netbios, isLoading: loadingNetbios } = useNetbios(20);
|
||||
const { data: mdnsHostnames, isLoading: loadingMdns } = useMdnsHostnames(20);
|
||||
|
||||
// Client Fingerprints Tab
|
||||
const { data: discoveryOs, isLoading: loadingOs } = useDiscoveryOs(20);
|
||||
const { data: dhcpFingerprints, isLoading: loadingDhcp } = useDhcpFingerprints(20);
|
||||
const { data: httpUserAgents, isLoading: loadingHttpUa } = useHttpUserAgents(20);
|
||||
|
||||
// Specific Protocols Tab
|
||||
const { data: sshVersions, isLoading: loadingSsh } = useSshVersions(20);
|
||||
const { data: bittorrentHashes, isLoading: loadingBittorrent } = useBittorrentHashes(20);
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes === 0 || !bytes) return "0 B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white tracking-tight">DPI Analytics</h1>
|
||||
<p className="text-slate-400 mt-2">Deep Packet Inspection metadata and fingerprinting.</p>
|
||||
</div>
|
||||
|
||||
<Tabs>
|
||||
<Tab label="Hostnames & Servers">
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>SNI Hostnames (TLS)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={sniHostnames}
|
||||
isLoading={loadingSni}
|
||||
columns={[
|
||||
{ header: "Hostname", accessor: (row: any) => row.sni_hostname },
|
||||
{ header: "Total", accessor: (row) => formatBytes(row.total) },
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>SSL Server Common Names</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={sslServerCn}
|
||||
isLoading={loadingSslCn}
|
||||
columns={[
|
||||
{ header: "Common Name", accessor: (row: any) => row.ssl_server_cn },
|
||||
{ header: "Total", accessor: (row) => formatBytes(row.total) },
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>QUIC Hostnames</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={quicHostnames}
|
||||
isLoading={loadingQuic}
|
||||
columns={[
|
||||
{ header: "Hostname", accessor: (row: any) => row.quic_hostname },
|
||||
{ header: "Total", accessor: (row) => formatBytes(row.total) },
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>NetBIOS Hostnames</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={netbios}
|
||||
isLoading={loadingNetbios}
|
||||
columns={[
|
||||
{ header: "Hostname", accessor: (row: any) => row.hostname },
|
||||
{ header: "Total", accessor: (row) => formatBytes(row.total) },
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="xl:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>mDNS Hostnames</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={mdnsHostnames}
|
||||
isLoading={loadingMdns}
|
||||
columns={[
|
||||
{ header: "Hostname", accessor: (row: any) => row.mdns_hostname },
|
||||
{ header: "Total", accessor: (row) => formatBytes(row.total) },
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</Tab>
|
||||
|
||||
<Tab label="Client Fingerprints">
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
||||
<Card className="xl:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Operating Systems Discovery</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={discoveryOs}
|
||||
isLoading={loadingOs}
|
||||
columns={[
|
||||
{ header: "Operating System", accessor: (row: any) => row.os_label },
|
||||
{ header: "Download", accessor: (row) => formatBytes(row.download) },
|
||||
{ header: "Upload", accessor: (row) => formatBytes(row.upload) },
|
||||
{ header: "Total", accessor: (row) => formatBytes(row.total) },
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>DHCP Fingerprints</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={dhcpFingerprints}
|
||||
isLoading={loadingDhcp}
|
||||
columns={[
|
||||
{ header: "Fingerprint", accessor: (row: any) => row.fingerprint },
|
||||
{ header: "Total", accessor: (row) => formatBytes(row.total) },
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>HTTP User Agents</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={httpUserAgents}
|
||||
isLoading={loadingHttpUa}
|
||||
columns={[
|
||||
{ header: "User Agent", accessor: (row: any) => row.user_agent },
|
||||
{ header: "Total", accessor: (row) => formatBytes(row.total) },
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</Tab>
|
||||
|
||||
<Tab label="Specific Protocols">
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>SSH Versions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={sshVersions}
|
||||
isLoading={loadingSsh}
|
||||
columns={[
|
||||
{ header: "SSH Version", accessor: (row: any) => row.ssh_version },
|
||||
{ header: "Total", accessor: (row) => formatBytes(row.total) },
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>BitTorrent Hashes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={bittorrentHashes}
|
||||
isLoading={loadingBittorrent}
|
||||
columns={[
|
||||
{ header: "Label", accessor: (row: any) => row.label },
|
||||
{ header: "Info Hash", accessor: (row: any) => row.info_hash },
|
||||
{ header: "Total", accessor: (row) => formatBytes(row.total) },
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useEvents } from "@/lib/api";
|
||||
import { DataTable, Column } from "@/components/ui/DataTable";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
||||
import { Loader2, AlertCircle, Info, AlertTriangle } from "lucide-react";
|
||||
|
||||
export default function EventsPage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { data, isLoading } = useEvents(100); // fetch 100 events
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
const getSeverityIcon = (severity: string) => {
|
||||
switch (severity.toLowerCase()) {
|
||||
case 'critical':
|
||||
case 'high':
|
||||
return <AlertCircle className="w-4 h-4 text-red-500" />;
|
||||
case 'medium':
|
||||
case 'warning':
|
||||
return <AlertTriangle className="w-4 h-4 text-yellow-500" />;
|
||||
default:
|
||||
return <Info className="w-4 h-4 text-blue-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
const columns: Column<typeof data[0]>[] = [
|
||||
{ header: "#", accessor: (row, i) => i + 1 },
|
||||
{
|
||||
header: "Timestamp",
|
||||
accessor: (row) => {
|
||||
try {
|
||||
return new Date(row.timestamp).toLocaleString();
|
||||
} catch {
|
||||
return row.timestamp;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
header: "Severity",
|
||||
accessor: (row) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{getSeverityIcon(row.severity)}
|
||||
<span className="capitalize">{row.severity}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{ header: "Event Type", accessor: (row) => row.event_type },
|
||||
{
|
||||
header: "Source IP",
|
||||
accessor: (row) => <span className="text-blue-400">{row.source_ip || "-"}</span>
|
||||
},
|
||||
{ header: "Message", accessor: (row) => row.message }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold tracking-tight text-white flex items-center gap-3">
|
||||
Network Events
|
||||
{isLoading && <Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Recent Events ({data.length})</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
searchPlaceholder="Search by event type, severity, IP, or message..."
|
||||
searchFilter={(row, query) => {
|
||||
const q = query.toLowerCase();
|
||||
return (
|
||||
(row.event_type || "").toLowerCase().includes(q) ||
|
||||
(row.severity || "").toLowerCase().includes(q) ||
|
||||
(row.source_ip || "").toLowerCase().includes(q) ||
|
||||
(row.message || "").toLowerCase().includes(q)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useFlows } from "@/lib/api";
|
||||
import { DataTable, Column } from "@/components/ui/DataTable";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { fmtBytes } from "@/lib/utils";
|
||||
|
||||
export default function FlowsPage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { data, isLoading } = useFlows(100); // fetch 100 flows
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
const columns: Column<typeof data[0]>[] = [
|
||||
{ header: "#", accessor: (row, i) => i + 1 },
|
||||
{ header: "Flow ID", accessor: (row) => row.flow_id },
|
||||
{ header: "Src IP", accessor: (row) => <span className="text-blue-400">{row.src_ip}</span> },
|
||||
{ header: "Dst IP", accessor: (row) => <span className="text-blue-400">{row.dst_ip}</span> },
|
||||
{ header: "Dst Port", accessor: (row) => row.dst_port },
|
||||
{ header: "Protocol", accessor: (row) => row.protocol },
|
||||
{ header: "App / Domain", accessor: (row) => row.app_label || row.domain || "-" },
|
||||
{
|
||||
header: "Download",
|
||||
accessor: (row) => <span className="text-blue-400">{fmtBytes(row.bytes_download)}</span>
|
||||
},
|
||||
{
|
||||
header: "Upload",
|
||||
accessor: (row) => <span className="text-green-500">{fmtBytes(row.bytes_upload)}</span>
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold tracking-tight text-white flex items-center gap-3">
|
||||
Active Flows
|
||||
{isLoading && <Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Active Flows ({data.length})</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
searchPlaceholder="Search flows by IP, App, or Domain..."
|
||||
searchFilter={(row, query) => {
|
||||
const q = query.toLowerCase();
|
||||
return (
|
||||
(row.src_ip || "").toLowerCase().includes(q) ||
|
||||
(row.dst_ip || "").toLowerCase().includes(q) ||
|
||||
(row.app_label || "").toLowerCase().includes(q) ||
|
||||
(row.domain || "").toLowerCase().includes(q) ||
|
||||
(row.protocol || "").toLowerCase().includes(q)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useCountries } from "@/lib/api";
|
||||
import { DataTable, Column } from "@/components/ui/DataTable";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { fmtBytes } from "@/lib/utils";
|
||||
import { WorldMap } from "@/components/ui/WorldMap";
|
||||
|
||||
export default function GeographyPage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { data, isLoading } = useCountries(50); // fetch 50 countries
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const mapData = useMemo(() => {
|
||||
if (!data) return [];
|
||||
return data.map((row) => ({
|
||||
countryCode: row.country_code,
|
||||
value: row.download + row.upload,
|
||||
label: row.country_name,
|
||||
}));
|
||||
}, [data]);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
const columns: Column<typeof data[0]>[] = [
|
||||
{ header: "#", accessor: (row, i) => i + 1 },
|
||||
{
|
||||
header: "Country",
|
||||
accessor: (row) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{row.country_code && (
|
||||
<img
|
||||
src={`https://flagcdn.com/w20/${row.country_code.toLowerCase()}.png`}
|
||||
alt={row.country_code}
|
||||
className="w-5 h-auto rounded-sm"
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span>{row.country_name}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{ header: "Country Code", accessor: (row) => row.country_code },
|
||||
{
|
||||
header: "Download",
|
||||
accessor: (row) => <span className="text-blue-400">{fmtBytes(row.download)}</span>
|
||||
},
|
||||
{
|
||||
header: "Upload",
|
||||
accessor: (row) => <span className="text-green-500">{fmtBytes(row.upload)}</span>
|
||||
},
|
||||
{
|
||||
header: "Total Data",
|
||||
accessor: (row) => <span className="font-medium text-white">{fmtBytes(row.download + row.upload)}</span>
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-in pb-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight text-white flex items-center gap-3">
|
||||
Geographic Traffic
|
||||
{isLoading && <Loader2 className="w-5 h-5 animate-spin text-primary" />}
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-1">Visualize data flow and activity across the globe.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="bg-card/80 backdrop-blur-xl border-border/50 animate-slide-up" style={{ animationDelay: '100ms' }}>
|
||||
<CardContent className="p-1 sm:p-4">
|
||||
<WorldMap data={mapData} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card/80 backdrop-blur-xl border-border/50 animate-slide-up" style={{ animationDelay: '200ms' }}>
|
||||
<CardHeader className="border-b border-border/50 pb-4 mb-4">
|
||||
<CardTitle className="text-lg text-white">Traffic by Country ({data.length})</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
searchPlaceholder="Search by country name..."
|
||||
searchFilter={(row, query) => {
|
||||
const q = query.toLowerCase();
|
||||
return (row.country_name || "").toLowerCase().includes(q);
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+65
-14
@@ -1,26 +1,77 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
|
||||
--color-border: var(--border);
|
||||
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
|
||||
--color-sidebar: var(--sidebar);
|
||||
|
||||
--font-sans: var(--font-inter);
|
||||
--font-mono: var(--font-inter);
|
||||
|
||||
/* Micro-animations */
|
||||
--animate-fade-in: fade-in 0.5s ease-out forwards;
|
||||
--animate-slide-up: slide-up 0.5s ease-out forwards;
|
||||
|
||||
@keyframes fade-in {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slide-up {
|
||||
0% { opacity: 0; transform: translateY(20px); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Premium Dark Theme */
|
||||
--background: #09090b; /* Very dark zinc */
|
||||
--foreground: #fafafa;
|
||||
|
||||
--card: rgba(24, 24, 27, 0.7); /* Zinc 900 with transparency for glassmorphism */
|
||||
--card-foreground: #fafafa;
|
||||
|
||||
--border: rgba(255, 255, 255, 0.1);
|
||||
|
||||
--primary: #3b82f6; /* Blue 500 */
|
||||
--primary-foreground: #ffffff;
|
||||
|
||||
--secondary: rgba(39, 39, 42, 0.8); /* Zinc 800 */
|
||||
--secondary-foreground: #fafafa;
|
||||
|
||||
--muted: rgba(39, 39, 42, 0.5);
|
||||
--muted-foreground: #a1a1aa; /* Zinc 400 */
|
||||
|
||||
--destructive: #ef4444; /* Red 500 */
|
||||
--destructive-foreground: #ffffff;
|
||||
|
||||
--sidebar: #09090b;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-family: var(--font-sans), sans-serif;
|
||||
background-image:
|
||||
radial-gradient(at 0% 0%, rgba(59, 130, 246, 0.15) 0px, transparent 50%),
|
||||
radial-gradient(at 100% 0%, rgba(139, 92, 246, 0.15) 0px, transparent 50%);
|
||||
background-attachment: fixed;
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useIntelligenceStats } from "@/lib/api";
|
||||
import {
|
||||
useIntelCryptoMining, useIntelTorDetection, useIntelVpnDetection,
|
||||
useIntelIpReputation, useIntelInsecureProtocols, useIntelUnencryptedPasswords,
|
||||
useIntelEncryptionAudit, useIntelDeviceDiscovery, useIntelServerDiscovery
|
||||
} from "@/lib/api-advanced";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
||||
import { DataTable, Column } from "@/components/ui/DataTable";
|
||||
import { Loader2, Bitcoin, Shield, Server, Lock, AlertTriangle, Key, Globe, Network, ShieldAlert } from "lucide-react";
|
||||
|
||||
export default function IntelligencePage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { data: stats, isLoading: loadingStats } = useIntelligenceStats();
|
||||
const [activeTab, setActiveTab] = useState<string>("Crypto Mining");
|
||||
|
||||
// Advanced data hooks
|
||||
const { data: cryptoMining, isLoading: loadingCrypto } = useIntelCryptoMining(20);
|
||||
const { data: torDetection, isLoading: loadingTor } = useIntelTorDetection(20);
|
||||
const { data: vpnDetection, isLoading: loadingVpn } = useIntelVpnDetection(20);
|
||||
const { data: ipReputation, isLoading: loadingIpRep } = useIntelIpReputation(20);
|
||||
const { data: insecureProtocols, isLoading: loadingInsecure } = useIntelInsecureProtocols(20);
|
||||
const { data: unencryptedPasswords, isLoading: loadingPasswords } = useIntelUnencryptedPasswords(20);
|
||||
const { data: encryptionAudit, isLoading: loadingEncryption } = useIntelEncryptionAudit(20);
|
||||
const { data: deviceDiscovery, isLoading: loadingDevice } = useIntelDeviceDiscovery(20);
|
||||
const { data: serverDiscovery, isLoading: loadingServer } = useIntelServerDiscovery(20);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
const intelItems = [
|
||||
{
|
||||
title: "Crypto Mining",
|
||||
value: stats?.intel_crypto_mining || 0,
|
||||
icon: <Bitcoin className="h-5 w-5 text-yellow-500" />,
|
||||
description: "Devices actively mining cryptocurrency"
|
||||
},
|
||||
{
|
||||
title: "Tor Detection",
|
||||
value: stats?.intel_tor_detection || 0,
|
||||
icon: <Globe className="h-5 w-5 text-purple-500" />,
|
||||
description: "Connections using the Tor network"
|
||||
},
|
||||
{
|
||||
title: "VPN Detection",
|
||||
value: stats?.intel_vpn_detection || 0,
|
||||
icon: <Shield className="h-5 w-5 text-blue-500" />,
|
||||
description: "Active Virtual Private Network tunnels"
|
||||
},
|
||||
{
|
||||
title: "IP Reputation",
|
||||
value: stats?.intel_ip_reputation || 0,
|
||||
icon: <ShieldAlert className="h-5 w-5 text-red-500" />,
|
||||
description: "Connections to malicious IP addresses"
|
||||
},
|
||||
{
|
||||
title: "Insecure Protocols",
|
||||
value: stats?.intel_insecure_protocols || 0,
|
||||
icon: <AlertTriangle className="h-5 w-5 text-orange-500" />,
|
||||
description: "Legacy clear-text protocols"
|
||||
},
|
||||
{
|
||||
title: "Unencrypted Passwords",
|
||||
value: stats?.intel_unencrypted_passwords || 0,
|
||||
icon: <Key className="h-5 w-5 text-red-400" />,
|
||||
description: "Credentials transmitted in plain text"
|
||||
},
|
||||
{
|
||||
title: "Encryption Audit",
|
||||
value: stats?.intel_encryption_audit || 0,
|
||||
icon: <Lock className="h-5 w-5 text-green-500" />,
|
||||
description: "Encrypted connections audited"
|
||||
},
|
||||
{
|
||||
title: "Device Discovery",
|
||||
value: stats?.intel_device_discovery || 0,
|
||||
icon: <Network className="h-5 w-5 text-cyan-500" />,
|
||||
description: "Unique client devices identified"
|
||||
},
|
||||
{
|
||||
title: "Server Discovery",
|
||||
value: stats?.intel_server_discovery || 0,
|
||||
icon: <Server className="h-5 w-5 text-gray-400" />,
|
||||
description: "Local servers running services"
|
||||
}
|
||||
];
|
||||
|
||||
const renderActiveTable = () => {
|
||||
switch(activeTab) {
|
||||
case "Crypto Mining":
|
||||
return (
|
||||
<DataTable
|
||||
data={cryptoMining} isLoading={loadingCrypto}
|
||||
columns={[
|
||||
{ header: "IP Address", accessor: (row: any) => row.ip_address },
|
||||
{ header: "MAC Address", accessor: (row: any) => row.mac_address },
|
||||
{ header: "Pool Host", accessor: (row: any) => row.pool_host },
|
||||
{ header: "App", accessor: (row: any) => row.app_label },
|
||||
{ header: "Confidence", accessor: (row: any) => row.confidence },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case "Tor Detection":
|
||||
return (
|
||||
<DataTable
|
||||
data={torDetection} isLoading={loadingTor}
|
||||
columns={[
|
||||
{ header: "IP Address", accessor: (row: any) => row.ip_address },
|
||||
{ header: "Exit Node", accessor: (row: any) => row.exit_node },
|
||||
{ header: "Country", accessor: (row: any) => row.country },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case "VPN Detection":
|
||||
return (
|
||||
<DataTable
|
||||
data={vpnDetection} isLoading={loadingVpn}
|
||||
columns={[
|
||||
{ header: "IP Address", accessor: (row: any) => row.ip_address },
|
||||
{ header: "VPN Type", accessor: (row: any) => row.vpn_type },
|
||||
{ header: "Remote IP", accessor: (row: any) => row.remote_ip },
|
||||
{ header: "Country", accessor: (row: any) => row.country },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case "IP Reputation":
|
||||
return (
|
||||
<DataTable
|
||||
data={ipReputation} isLoading={loadingIpRep}
|
||||
columns={[
|
||||
{ header: "IP Address", accessor: (row: any) => row.ip_address },
|
||||
{ header: "Reputation", accessor: (row: any) => row.reputation },
|
||||
{ header: "App", accessor: (row: any) => row.app_label },
|
||||
{ header: "Country", accessor: (row: any) => row.country },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case "Insecure Protocols":
|
||||
return (
|
||||
<DataTable
|
||||
data={insecureProtocols} isLoading={loadingInsecure}
|
||||
columns={[
|
||||
{ header: "Protocol", accessor: (row: any) => row.protocol },
|
||||
{ header: "IP Address", accessor: (row: any) => row.ip_address },
|
||||
{ header: "Risk", accessor: (row: any) => row.risk },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case "Unencrypted Passwords":
|
||||
return (
|
||||
<DataTable
|
||||
data={unencryptedPasswords} isLoading={loadingPasswords}
|
||||
columns={[
|
||||
{ header: "Protocol", accessor: (row: any) => row.protocol },
|
||||
{ header: "IP Address", accessor: (row: any) => row.ip_address },
|
||||
{ header: "Severity", accessor: (row: any) => row.severity },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case "Encryption Audit":
|
||||
return (
|
||||
<DataTable
|
||||
data={encryptionAudit} isLoading={loadingEncryption}
|
||||
columns={[
|
||||
{ header: "IP Address", accessor: (row: any) => row.ip_address },
|
||||
{ header: "Encrypted %", accessor: (row) => `${row.encrypted_pct}%` },
|
||||
{ header: "Risk Level", accessor: (row: any) => row.risk_level },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case "Device Discovery":
|
||||
return (
|
||||
<DataTable
|
||||
data={deviceDiscovery} isLoading={loadingDevice}
|
||||
columns={[
|
||||
{ header: "IP Address", accessor: (row: any) => row.ip_address },
|
||||
{ header: "Device Type", accessor: (row: any) => row.device_type },
|
||||
{ header: "OS", accessor: (row: any) => row.os_label },
|
||||
{ header: "Manufacturer", accessor: (row: any) => row.manufacturer },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case "Server Discovery":
|
||||
return (
|
||||
<DataTable
|
||||
data={serverDiscovery} isLoading={loadingServer}
|
||||
columns={[
|
||||
{ header: "IP Address", accessor: (row: any) => row.ip_address },
|
||||
{ header: "Server Type", accessor: (row: any) => row.server_type },
|
||||
{ header: "Port", accessor: (row: any) => row.port },
|
||||
{ header: "OS", accessor: (row: any) => row.os_label },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
default: return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold tracking-tight text-white flex items-center gap-3">
|
||||
Deep Packet Inspection Intelligence
|
||||
{loadingStats && <Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{intelItems.map((item, idx) => {
|
||||
const isActive = activeTab === item.title;
|
||||
return (
|
||||
<Card
|
||||
key={idx}
|
||||
onClick={() => setActiveTab(item.title)}
|
||||
className={`
|
||||
bg-card/50 backdrop-blur-sm border-border/50 hover:bg-card/80 transition-all cursor-pointer
|
||||
${isActive ? 'ring-2 ring-primary border-transparent' : ''}
|
||||
`}
|
||||
>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-white">{item.title}</CardTitle>
|
||||
{item.icon}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold text-white mb-1">
|
||||
{item.value.toLocaleString()}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{item.description}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Detailed Report: {activeTab}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{renderActiveTable()}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+10
-12
@@ -1,20 +1,16 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { DashboardLayout } from "@/components/layout/DashboardLayout";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
const inter = Inter({
|
||||
variable: "--font-inter",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "Netify Dashboard",
|
||||
description: "Netify Network Intelligence Dashboard",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -25,9 +21,11 @@ export default function RootLayout({
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
className={`${inter.variable} h-full antialiased dark`}
|
||||
>
|
||||
<body className="min-h-full flex flex-col">{children}</body>
|
||||
<body className="min-h-full flex flex-col">
|
||||
<DashboardLayout>{children}</DashboardLayout>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useLookupApplications, LookupApp } from "@/lib/api";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
||||
import { Loader2, Search, AppWindow, HelpCircle, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react";
|
||||
|
||||
export default function LookupPage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [debouncedSearch, setDebouncedSearch] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const limit = 24; // 24 items per page fits a 2, 3, or 4 column grid perfectly
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
// Debounce search query input to avoid spamming requests
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedSearch(searchQuery);
|
||||
setPage(1); // Reset page on new search
|
||||
}, 400);
|
||||
|
||||
return () => clearTimeout(handler);
|
||||
}, [searchQuery]);
|
||||
|
||||
const { data, isLoading, error } = useLookupApplications(page, limit, debouncedSearch);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
const applications = data?.applications || [];
|
||||
const pagination = data?.pagination || { total_records: 0, total_pages: 1, current_page: 1 };
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-fade-in pb-10">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white tracking-tight flex items-center gap-3">
|
||||
Application Database
|
||||
{isLoading && <Loader2 className="w-5 h-5 animate-spin text-primary" />}
|
||||
</h1>
|
||||
<p className="text-slate-400 mt-2">
|
||||
Search Netify's extensive classification database of over 2,500+ apps, protocols, and services.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Search Input Bar */}
|
||||
<div className="relative max-w-md w-full animate-slide-up" style={{ animationDelay: '50ms' }}>
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by app name or tag (e.g. facebook, openvpn)..."
|
||||
className="w-full bg-card/60 backdrop-blur-xl border border-border/60 rounded-xl pl-10 pr-4 py-3 text-white placeholder-slate-400 focus:outline-none focus:border-primary/80 focus:ring-1 focus:ring-primary/80 transition-all duration-200"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Cards Grid */}
|
||||
{error ? (
|
||||
<div className="p-8 bg-red-950/20 border border-red-500/30 rounded-xl text-center text-red-400">
|
||||
<p className="font-semibold">Failed to load application lookup database</p>
|
||||
<p className="text-sm mt-1">{error.message}</p>
|
||||
</div>
|
||||
) : applications.length === 0 && !isLoading ? (
|
||||
<div className="p-16 bg-card/40 border border-border/50 rounded-2xl text-center text-slate-400 max-w-lg mx-auto">
|
||||
<AppWindow className="h-12 w-12 mx-auto mb-4 text-slate-500" />
|
||||
<p className="font-semibold text-white">No applications found</p>
|
||||
<p className="text-sm mt-1">Try refining your search keyword or clearing the input.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 animate-slide-up" style={{ animationDelay: '100ms' }}>
|
||||
{applications.map((app: LookupApp) => (
|
||||
<Card
|
||||
key={app.id}
|
||||
className="group relative overflow-hidden bg-card/60 backdrop-blur-xl border-border/50 hover:bg-card/100 hover:border-primary/40 transition-all duration-300 flex flex-col justify-between"
|
||||
>
|
||||
<div className="p-5 flex flex-col h-full justify-between gap-4">
|
||||
{/* Top: Icon + Title */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="h-10 w-10 rounded-lg bg-secondary/80 flex items-center justify-center overflow-hidden border border-border/40 group-hover:border-primary/20 transition-all">
|
||||
{app.favicon || app.logo ? (
|
||||
<img
|
||||
src={app.favicon || app.logo}
|
||||
alt={app.label}
|
||||
className="h-6 w-6 object-contain"
|
||||
onError={(e) => {
|
||||
// If image fails, replace with default icon
|
||||
(e.target as HTMLElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<AppWindow className="h-5 w-5 text-slate-400" />
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[10px] uppercase font-semibold tracking-wider text-primary bg-primary/10 border border-primary/20 px-2 py-0.5 rounded-full">
|
||||
{app.application_category?.label || "Unclassified"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-bold text-white group-hover:text-primary transition-colors text-base line-clamp-1">
|
||||
{app.label}
|
||||
</h3>
|
||||
<p className="text-[11px] text-slate-500 font-mono mt-0.5">{app.tag}</p>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-400 line-clamp-3 leading-relaxed mt-2">
|
||||
{app.description || "No description provided for this application definition."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Bottom: ID and system information */}
|
||||
<div className="flex items-center justify-between pt-2 border-t border-border/30 text-[10px] text-slate-500 font-mono">
|
||||
<span>ID: {app.id}</span>
|
||||
<span>{app.name || "N/A"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Pagination Controls */}
|
||||
{pagination.total_pages > 1 && (
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 pt-4 border-t border-border/50 text-slate-400 text-sm animate-slide-up" style={{ animationDelay: '150ms' }}>
|
||||
<div>
|
||||
Showing <span className="text-white font-medium">{((page - 1) * limit) + 1}</span> to{" "}
|
||||
<span className="text-white font-medium">
|
||||
{Math.min(page * limit, pagination.total_records)}
|
||||
</span>{" "}
|
||||
of <span className="text-white font-medium">{pagination.total_records}</span> entries
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
{/* First Page */}
|
||||
<button
|
||||
onClick={() => setPage(1)}
|
||||
disabled={page === 1 || isLoading}
|
||||
className="p-2 rounded-lg bg-card border border-border/50 text-slate-400 hover:bg-secondary disabled:opacity-30 disabled:hover:bg-card transition-colors"
|
||||
title="First Page"
|
||||
>
|
||||
<ChevronsLeft className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
{/* Prev Page */}
|
||||
<button
|
||||
onClick={() => setPage(p => Math.max(p - 1, 1))}
|
||||
disabled={page === 1 || isLoading}
|
||||
className="p-2 rounded-lg bg-card border border-border/50 text-slate-400 hover:bg-secondary disabled:opacity-30 disabled:hover:bg-card transition-colors"
|
||||
title="Previous Page"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
{/* Page indicators */}
|
||||
<span className="px-4 py-1.5 rounded-lg bg-secondary/50 border border-border/30 text-white font-mono">
|
||||
{page} / {pagination.total_pages}
|
||||
</span>
|
||||
|
||||
{/* Next Page */}
|
||||
<button
|
||||
onClick={() => setPage(p => Math.min(p + 1, pagination.total_pages))}
|
||||
disabled={page === pagination.total_pages || isLoading}
|
||||
className="p-2 rounded-lg bg-card border border-border/50 text-slate-400 hover:bg-secondary disabled:opacity-30 disabled:hover:bg-card transition-colors"
|
||||
title="Next Page"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
{/* Last Page */}
|
||||
<button
|
||||
onClick={() => setPage(pagination.total_pages)}
|
||||
disabled={page === pagination.total_pages || isLoading}
|
||||
className="p-2 rounded-lg bg-card border border-border/50 text-slate-400 hover:bg-secondary disabled:opacity-30 disabled:hover:bg-card transition-colors"
|
||||
title="Last Page"
|
||||
>
|
||||
<ChevronsRight className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Tabs, Tab } from "@/components/ui/Tabs";
|
||||
import { DataTable, Column } from "@/components/ui/DataTable";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
||||
import {
|
||||
useRegions, useCities, useVlans, useInterfaces,
|
||||
useIpVersions, useRemoteIps, useMacBandwidth,
|
||||
useFlowTypes, useFlowOrigins
|
||||
} from "@/lib/api-advanced";
|
||||
|
||||
export default function NetworkInfrastructurePage() {
|
||||
// Routing & Geography Tab
|
||||
const { data: regions, isLoading: loadingRegions } = useRegions(20);
|
||||
const { data: cities, isLoading: loadingCities } = useCities(20);
|
||||
|
||||
// Interfaces & VLANs Tab
|
||||
const { data: interfaces, isLoading: loadingInterfaces } = useInterfaces(20);
|
||||
const { data: vlans, isLoading: loadingVlans } = useVlans(20);
|
||||
|
||||
// IP & MAC Tab
|
||||
const { data: remoteIps, isLoading: loadingRemoteIps } = useRemoteIps(20);
|
||||
const { data: ipVersions, isLoading: loadingIpVersions } = useIpVersions(20);
|
||||
const { data: macBandwidth, isLoading: loadingMacBandwidth } = useMacBandwidth(20);
|
||||
|
||||
// Flow Profiles Tab
|
||||
const { data: flowTypes, isLoading: loadingFlowTypes } = useFlowTypes(20);
|
||||
const { data: flowOrigins, isLoading: loadingFlowOrigins } = useFlowOrigins(20);
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes === 0 || !bytes) return "0 B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white tracking-tight">Network Infrastructure</h1>
|
||||
<p className="text-slate-400 mt-2">Physical, logical, and geographic network topologies.</p>
|
||||
</div>
|
||||
|
||||
<Tabs>
|
||||
<Tab label="Routing & Geography">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Top Regions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={regions}
|
||||
isLoading={loadingRegions}
|
||||
columns={[
|
||||
{ header: "Region", accessor: (row: any) => row.region_name },
|
||||
{ header: "Country", accessor: (row: any) => row.country_name },
|
||||
{ header: "Download", accessor: (row) => formatBytes(row.download) },
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Top Cities</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={cities}
|
||||
isLoading={loadingCities}
|
||||
columns={[
|
||||
{ header: "City", accessor: (row: any) => row.city_name },
|
||||
{ header: "Region", accessor: (row: any) => row.region_name },
|
||||
{ header: "Country", accessor: (row: any) => row.country_name },
|
||||
{ header: "Download", accessor: (row) => formatBytes(row.download) },
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</Tab>
|
||||
|
||||
<Tab label="Interfaces & VLANs">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Network Interfaces</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={interfaces}
|
||||
isLoading={loadingInterfaces}
|
||||
columns={[
|
||||
{ header: "Interface", accessor: (row: any) => row.iface_name },
|
||||
{ header: "Role", accessor: (row: any) => row.iface_role },
|
||||
{ header: "Download", accessor: (row) => formatBytes(row.download) },
|
||||
{ header: "Upload", accessor: (row) => formatBytes(row.upload) },
|
||||
{ header: "Total", accessor: (row) => formatBytes(row.total) },
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>VLANs</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={vlans}
|
||||
isLoading={loadingVlans}
|
||||
columns={[
|
||||
{ header: "VLAN ID", accessor: (row: any) => row.vlan_id },
|
||||
{ header: "Label", accessor: (row: any) => row.vlan_label },
|
||||
{ header: "Download", accessor: (row) => formatBytes(row.download) },
|
||||
{ header: "Upload", accessor: (row) => formatBytes(row.upload) },
|
||||
{ header: "Total", accessor: (row) => formatBytes(row.total) },
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</Tab>
|
||||
|
||||
<Tab label="IP & MAC Properties">
|
||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
||||
<div className="xl:col-span-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Top Remote IPs</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={remoteIps}
|
||||
isLoading={loadingRemoteIps}
|
||||
columns={[
|
||||
{ header: "IP Address", accessor: (row: any) => row.remote_ip },
|
||||
{ header: "IP Version", accessor: (row) => `IPv${row.ip_version}` },
|
||||
{ header: "Download", accessor: (row) => formatBytes(row.download) },
|
||||
{ header: "Upload", accessor: (row) => formatBytes(row.upload) },
|
||||
{ header: "Total", accessor: (row) => formatBytes(row.total) },
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>IP Versions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={ipVersions}
|
||||
isLoading={loadingIpVersions}
|
||||
columns={[
|
||||
{ header: "Version", accessor: (row: any) => row.ip_version_label },
|
||||
{ header: "Total Traffic", accessor: (row) => formatBytes(row.total) },
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>MAC Bandwidth</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={macBandwidth}
|
||||
isLoading={loadingMacBandwidth}
|
||||
columns={[
|
||||
{ header: "MAC Address", accessor: (row: any) => row.mac_address },
|
||||
{ header: "Manufacturer", accessor: (row: any) => row.manufacturer },
|
||||
{ header: "Total", accessor: (row) => formatBytes(row.total) },
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
|
||||
<Tab label="Flow Profiles">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Flow Types</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={flowTypes}
|
||||
isLoading={loadingFlowTypes}
|
||||
columns={[
|
||||
{ header: "Flow Type", accessor: (row: any) => row.flow_type_label },
|
||||
{ header: "Download", accessor: (row) => formatBytes(row.download) },
|
||||
{ header: "Upload", accessor: (row) => formatBytes(row.upload) },
|
||||
{ header: "Total", accessor: (row) => formatBytes(row.total) },
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Flow Origins</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={flowOrigins}
|
||||
isLoading={loadingFlowOrigins}
|
||||
columns={[
|
||||
{ header: "Origin", accessor: (row: any) => row.flow_origin_label },
|
||||
{ header: "Download", accessor: (row) => formatBytes(row.download) },
|
||||
{ header: "Upload", accessor: (row) => formatBytes(row.upload) },
|
||||
{ header: "Total", accessor: (row) => formatBytes(row.total) },
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
||||
import { DataTable, Column } from "@/components/ui/DataTable";
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer,
|
||||
PieChart, Pie, Cell
|
||||
} from "recharts";
|
||||
import { Activity, Loader2 } from "lucide-react";
|
||||
import { useAppCategories, useContinents, AppCategoryStat } from "@/lib/api";
|
||||
import { fmtBytes } from "@/lib/utils";
|
||||
|
||||
const COLORS = ['#58a6ff', '#3fb950', '#f0883e', '#bc8cff', '#f85149'];
|
||||
|
||||
export default function NetworkIntelligencePage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const appCategories = useAppCategories();
|
||||
const continents = useContinents();
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
const appCols: Column<AppCategoryStat>[] = [
|
||||
{ header: "#", accessor: (row, i) => i + 1 },
|
||||
{ header: "Category", accessor: (row) => <span className="font-semibold text-white">{row.category_label || '-'}</span> },
|
||||
{ header: "Download", accessor: (row) => <span className="text-cyan-400">{fmtBytes(row.download)}</span> },
|
||||
{ header: "Upload", accessor: (row) => <span className="text-green-400">{fmtBytes(row.upload)}</span> },
|
||||
{ header: "Total", accessor: (row) => fmtBytes(row.total) },
|
||||
];
|
||||
|
||||
const isLoading = appCategories.isLoading || continents.isLoading;
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-in pb-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight text-white flex items-center gap-3">
|
||||
Network Intelligence
|
||||
{isLoading && <Loader2 className="w-5 h-5 animate-spin text-primary" />}
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-1">Deep packet inspection analytics and geography.</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-blue-400 bg-blue-500/10 border border-blue-500/20 px-4 py-2 rounded-lg shadow-[0_0_15px_rgba(59,130,246,0.1)]">
|
||||
<Activity className="h-4 w-4" />
|
||||
<span>DPI Engine Active</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* App Categories */}
|
||||
<Card className="bg-card/80 backdrop-blur-xl border-border/50 animate-slide-up" style={{ animationDelay: '100ms' }}>
|
||||
<CardHeader className="border-b border-border/50 pb-4 mb-4">
|
||||
<CardTitle className="text-lg text-white">App Categories</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="h-[200px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={appCategories.data || []}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={80}
|
||||
paddingAngle={2}
|
||||
dataKey="download"
|
||||
nameKey="category_label"
|
||||
>
|
||||
{(appCategories.data || []).map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<RechartsTooltip
|
||||
contentStyle={{ backgroundColor: '#161b22', borderColor: '#30363d', color: '#c9d1d9' }}
|
||||
formatter={(val: any) => fmtBytes(val as number)}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<DataTable data={appCategories.data || []} columns={appCols} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Continents */}
|
||||
<Card className="bg-card/80 backdrop-blur-xl border-border/50 animate-slide-up" style={{ animationDelay: '200ms' }}>
|
||||
<CardHeader className="border-b border-border/50 pb-4 mb-4">
|
||||
<CardTitle className="text-lg text-white">Continents (Download Volume)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="h-[200px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={continents.data || []} layout="vertical" margin={{ top: 0, right: 30, left: 40, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#30363d" horizontal={true} vertical={false} />
|
||||
<XAxis type="number" stroke="#8b949e" tickFormatter={(v) => `${(v/1000000000).toFixed(1)}GB`} />
|
||||
<YAxis dataKey="continent_name" type="category" stroke="#8b949e" fontSize={12} />
|
||||
<RechartsTooltip
|
||||
cursor={{fill: '#21262d'}}
|
||||
contentStyle={{ backgroundColor: '#161b22', borderColor: '#30363d', color: '#c9d1d9' }}
|
||||
formatter={(val: any) => fmtBytes(val as number)}
|
||||
/>
|
||||
<Bar dataKey="download" fill="#58a6ff" radius={[0, 4, 4, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="mt-4 border border-border rounded-md p-4 bg-secondary/30">
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
Most of your traffic is bound for {continents.data && continents.data.length > 0 ? continents.data[0].continent_name : "unknown destinations"}. Ensure your firewall rules are optimized for latency across trans-oceanic cables.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+293
-56
@@ -1,65 +1,302 @@
|
||||
import Image from "next/image";
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
||||
import { Activity, Download, Upload, ShieldAlert, Zap, Server, Loader2 } from "lucide-react";
|
||||
import {
|
||||
AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer,
|
||||
BarChart, Bar,
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell
|
||||
} from "recharts";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useDashboardSummary, useTimeline, useTopApps, useTopProtocols } from "@/lib/api";
|
||||
import { fmtBytes } from "@/lib/utils";
|
||||
|
||||
const COLORS = ['#58a6ff', '#3fb950', '#f0883e', '#bc8cff', '#f85149'];
|
||||
|
||||
export default function SummaryPage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const summary = useDashboardSummary();
|
||||
const timeline = useTimeline(24);
|
||||
const topApps = useTopApps(7);
|
||||
const topProtocols = useTopProtocols();
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
const isLoading = summary.isLoading || timeline.isLoading || topApps.isLoading || topProtocols.isLoading;
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={100}
|
||||
height={20}
|
||||
priority
|
||||
<div className="space-y-8 animate-fade-in pb-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight text-white flex items-center gap-3">
|
||||
Summary Overview
|
||||
{isLoading && <Loader2 className="w-5 h-5 animate-spin text-primary" />}
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-1">Real-time network traffic and intelligence summary.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KPI Cards */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
||||
<Card className="group relative overflow-hidden bg-card/80 backdrop-blur-xl border-border/50 hover:bg-card/100 hover:border-primary/30 transition-all duration-300 cursor-default animate-slide-up" style={{ animationDelay: '0ms' }}>
|
||||
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
|
||||
<Download className="h-16 w-16 text-blue-500" />
|
||||
</div>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2 relative z-10">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground group-hover:text-blue-400 transition-colors">Download</CardTitle>
|
||||
<Download className="h-4 w-4 text-blue-500" />
|
||||
</CardHeader>
|
||||
<CardContent className="relative z-10">
|
||||
<div className="text-3xl font-bold text-white tracking-tight">
|
||||
{summary.data ? fmtBytes(summary.data.bandwidth_down) : "0 B"}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="group relative overflow-hidden bg-card/80 backdrop-blur-xl border-border/50 hover:bg-card/100 hover:border-emerald-500/30 transition-all duration-300 cursor-default animate-slide-up" style={{ animationDelay: '50ms' }}>
|
||||
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
|
||||
<Upload className="h-16 w-16 text-emerald-500" />
|
||||
</div>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2 relative z-10">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground group-hover:text-emerald-400 transition-colors">Upload</CardTitle>
|
||||
<Upload className="h-4 w-4 text-emerald-500" />
|
||||
</CardHeader>
|
||||
<CardContent className="relative z-10">
|
||||
<div className="text-3xl font-bold text-white tracking-tight">
|
||||
{summary.data ? fmtBytes(summary.data.bandwidth_up) : "0 B"}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="group relative overflow-hidden bg-card/80 backdrop-blur-xl border-border/50 hover:bg-card/100 hover:border-orange-500/30 transition-all duration-300 cursor-default animate-slide-up" style={{ animationDelay: '100ms' }}>
|
||||
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
|
||||
<Server className="h-16 w-16 text-orange-500" />
|
||||
</div>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2 relative z-10">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground group-hover:text-orange-400 transition-colors">Devices</CardTitle>
|
||||
<Server className="h-4 w-4 text-orange-500" />
|
||||
</CardHeader>
|
||||
<CardContent className="relative z-10">
|
||||
<div className="text-3xl font-bold text-white tracking-tight">
|
||||
{summary.data?.total_devices?.toLocaleString() || "0"}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="group relative overflow-hidden bg-card/80 backdrop-blur-xl border-border/50 hover:bg-card/100 hover:border-purple-500/30 transition-all duration-300 cursor-default animate-slide-up" style={{ animationDelay: '150ms' }}>
|
||||
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
|
||||
<Activity className="h-16 w-16 text-purple-500" />
|
||||
</div>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2 relative z-10">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground group-hover:text-purple-400 transition-colors">Flows</CardTitle>
|
||||
<Activity className="h-4 w-4 text-purple-500" />
|
||||
</CardHeader>
|
||||
<CardContent className="relative z-10">
|
||||
<div className="text-3xl font-bold text-white tracking-tight">
|
||||
{summary.data?.active_flows?.toLocaleString() || "0"}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="group relative overflow-hidden bg-card/80 backdrop-blur-xl border-red-500/20 hover:bg-card/100 hover:border-red-500/50 transition-all duration-300 cursor-default animate-slide-up" style={{ animationDelay: '200ms' }}>
|
||||
<div className="absolute inset-0 bg-red-500/5 pointer-events-none" />
|
||||
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity text-red-500">
|
||||
<ShieldAlert className="h-16 w-16" />
|
||||
</div>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2 relative z-10">
|
||||
<CardTitle className="text-sm font-medium text-red-400 group-hover:text-red-300 transition-colors">Threats</CardTitle>
|
||||
<ShieldAlert className="h-4 w-4 text-red-500 animate-pulse" />
|
||||
</CardHeader>
|
||||
<CardContent className="relative z-10">
|
||||
<div className="text-3xl font-bold text-red-500 tracking-tight">
|
||||
{summary.data?.total_threats?.toLocaleString() || "0"}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="group relative overflow-hidden bg-card/80 backdrop-blur-xl border-border/50 hover:bg-card/100 hover:border-yellow-500/30 transition-all duration-300 cursor-default animate-slide-up" style={{ animationDelay: '250ms' }}>
|
||||
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity text-yellow-500">
|
||||
<Zap className="h-16 w-16" />
|
||||
</div>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2 relative z-10">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground group-hover:text-yellow-400 transition-colors">Events</CardTitle>
|
||||
<Zap className="h-4 w-4 text-yellow-500" />
|
||||
</CardHeader>
|
||||
<CardContent className="relative z-10">
|
||||
<div className="text-3xl font-bold text-white tracking-tight">
|
||||
{summary.data?.total_events?.toLocaleString() || "0"}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-7 animate-slide-up" style={{ animationDelay: '300ms' }}>
|
||||
{/* Timeline Chart */}
|
||||
<Card className="col-span-4 bg-card/80 backdrop-blur-xl border-border/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg text-white">Bandwidth Timeline</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pl-2">
|
||||
<div className="h-[350px] w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={timeline.data} margin={{ top: 15, right: 20, left: 10, bottom: 5 }}>
|
||||
<defs>
|
||||
<linearGradient id="colorDownload" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3}/>
|
||||
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0}/>
|
||||
</linearGradient>
|
||||
<linearGradient id="colorUpload" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#10b981" stopOpacity={0.3}/>
|
||||
<stop offset="95%" stopColor="#10b981" stopOpacity={0}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="fetched_at"
|
||||
stroke="#a1a1aa"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={10}
|
||||
tickFormatter={(val) => {
|
||||
try {
|
||||
return new Date(val).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
} catch (e) {
|
||||
return val;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
||||
To get started, edit the page.tsx file.
|
||||
</h1>
|
||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
||||
Looking for a starting point or more instructions? Head over to{" "}
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
<YAxis
|
||||
stroke="#a1a1aa"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(val) => {
|
||||
if (val === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(val) / Math.log(k));
|
||||
return parseFloat((val / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
}}
|
||||
/>
|
||||
<RechartsTooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'rgba(24, 24, 27, 0.9)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: '8px',
|
||||
color: '#fafafa',
|
||||
boxShadow: '0 4px 20px rgba(0,0,0,0.3)'
|
||||
}}
|
||||
itemStyle={{ color: '#fafafa', fontSize: '13px', fontWeight: 500 }}
|
||||
labelFormatter={(label) => {
|
||||
try {
|
||||
return new Date(label).toLocaleString();
|
||||
} catch (e) {
|
||||
return label;
|
||||
}
|
||||
}}
|
||||
formatter={(value: any, name: any) => [fmtBytes(value as number), name === 'total_download' ? 'Download' : 'Upload']}
|
||||
/>
|
||||
<Area type="monotone" dataKey="total_download" stroke="#3b82f6" strokeWidth={3} fillOpacity={1} fill="url(#colorDownload)" activeDot={{ r: 6, fill: "#3b82f6", stroke: "#18181b", strokeWidth: 2 }} />
|
||||
<Area type="monotone" dataKey="total_upload" stroke="#10b981" strokeWidth={3} fillOpacity={1} fill="url(#colorUpload)" activeDot={{ r: 6, fill: "#10b981", stroke: "#18181b", strokeWidth: 2 }} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Top Apps & Protocols */}
|
||||
<div className="col-span-3 space-y-6">
|
||||
<Card className="bg-card/80 backdrop-blur-xl border-border/50 flex-1">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg text-white">Top Apps (Download)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-[150px] w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={topApps.data} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" vertical={false} />
|
||||
<XAxis dataKey="app_label" stroke="#a1a1aa" fontSize={11} tickLine={false} axisLine={false} tickMargin={5} />
|
||||
<YAxis
|
||||
stroke="#a1a1aa"
|
||||
fontSize={11}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => {
|
||||
const mb = value / 1048576;
|
||||
return `${mb.toFixed(0)}M`;
|
||||
}}
|
||||
/>
|
||||
<RechartsTooltip
|
||||
cursor={{fill: 'rgba(255,255,255,0.05)'}}
|
||||
contentStyle={{
|
||||
backgroundColor: 'rgba(24, 24, 27, 0.9)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: '8px',
|
||||
color: '#fafafa',
|
||||
}}
|
||||
itemStyle={{ color: '#fafafa', fontSize: '13px', fontWeight: 500 }}
|
||||
formatter={(value: any) => [fmtBytes(value as number), 'Download']}
|
||||
/>
|
||||
<Bar dataKey="download" fill="#8b5cf6" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card/80 backdrop-blur-xl border-border/50 flex-1">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg text-white">Top Protocols</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-[150px] w-full flex items-center justify-center">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={topProtocols.data}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={45}
|
||||
outerRadius={70}
|
||||
paddingAngle={4}
|
||||
dataKey="download"
|
||||
nameKey="protocol_label"
|
||||
stroke="none"
|
||||
>
|
||||
Templates
|
||||
</a>{" "}
|
||||
or the{" "}
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Learning
|
||||
</a>{" "}
|
||||
center.
|
||||
</p>
|
||||
{topProtocols.data.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} className="hover:opacity-80 transition-opacity outline-none" />
|
||||
))}
|
||||
</Pie>
|
||||
<RechartsTooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'rgba(24, 24, 27, 0.9)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: '8px',
|
||||
color: '#fafafa'
|
||||
}}
|
||||
itemStyle={{ color: '#fafafa', fontSize: '13px', fontWeight: 500 }}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
{/* Custom Legend */}
|
||||
<div className="flex flex-col justify-center ml-2 space-y-3">
|
||||
{topProtocols.data.slice(0, 5).map((entry, index) => (
|
||||
<div key={`${entry.protocol_label}-${index}`} className="flex items-center text-sm text-muted-foreground whitespace-nowrap group cursor-default">
|
||||
<div className="w-3.5 h-3.5 rounded-full mr-3 shrink-0 shadow-sm" style={{ backgroundColor: COLORS[index % COLORS.length] }} />
|
||||
<span className="truncate max-w-[100px] group-hover:text-white transition-colors">{entry.protocol_label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Deploy Now
|
||||
</a>
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTlsVersions, useTlsCiphers, useTlsSecurity } from "@/lib/api";
|
||||
import { DataTable, Column } from "@/components/ui/DataTable";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
||||
import { Loader2, ShieldCheck, ShieldAlert, ShieldX } from "lucide-react";
|
||||
import { fmtBytes } from "@/lib/utils";
|
||||
import { PieChart, Pie, Cell, Tooltip as RechartsTooltip, ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid } from "recharts";
|
||||
|
||||
const COLORS = {
|
||||
Recommended: "#3fb950",
|
||||
Secure: "#58a6ff",
|
||||
Weak: "#f0883e",
|
||||
Insecure: "#f85149"
|
||||
};
|
||||
|
||||
export default function SecurityAuditPage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const versions = useTlsVersions(10);
|
||||
const ciphers = useTlsCiphers(20);
|
||||
const security = useTlsSecurity(10);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
const isLoading = versions.isLoading || ciphers.isLoading || security.isLoading;
|
||||
|
||||
const versionColumns: Column<typeof versions.data[0]>[] = [
|
||||
{ header: "#", accessor: (row, i) => i + 1 },
|
||||
{ header: "TLS Version", accessor: (row) => row.tls_version },
|
||||
{ header: "Download", accessor: (row) => <span className="text-blue-400">{fmtBytes(row.download)}</span> },
|
||||
{ header: "Upload", accessor: (row) => <span className="text-green-500">{fmtBytes(row.upload)}</span> },
|
||||
{ header: "Total Volume", accessor: (row) => <span className="font-medium text-white">{fmtBytes(row.total)}</span> }
|
||||
];
|
||||
|
||||
const cipherColumns: Column<typeof ciphers.data[0]>[] = [
|
||||
{ header: "#", accessor: (row, i) => i + 1 },
|
||||
{ header: "Cipher Suite", accessor: (row) => <span className="text-purple-400">{row.tls_cipher}</span> },
|
||||
{ header: "Download", accessor: (row) => <span className="text-blue-400">{fmtBytes(row.download)}</span> },
|
||||
{ header: "Upload", accessor: (row) => <span className="text-green-500">{fmtBytes(row.upload)}</span> },
|
||||
{ header: "Total Volume", accessor: (row) => <span className="font-medium text-white">{fmtBytes(row.total)}</span> }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold tracking-tight text-white flex items-center gap-3">
|
||||
Security Audit (TLS)
|
||||
{isLoading && <Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Security Overview Cards */}
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{security.data.map((item) => (
|
||||
<Card key={item.tls_security} className="bg-card/50 backdrop-blur-sm border-border/50">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
{item.tls_security} Connections
|
||||
</CardTitle>
|
||||
{item.tls_security === "Recommended" && <ShieldCheck className="h-4 w-4 text-green-500" />}
|
||||
{item.tls_security === "Secure" && <ShieldCheck className="h-4 w-4 text-blue-500" />}
|
||||
{item.tls_security === "Weak" && <ShieldAlert className="h-4 w-4 text-orange-500" />}
|
||||
{item.tls_security === "Insecure" && <ShieldX className="h-4 w-4 text-red-500" />}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-white">
|
||||
{fmtBytes(item.total)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{/* TLS Security Pie Chart */}
|
||||
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Security Level Distribution</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-[250px] w-full flex items-center justify-center">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={security.data}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={80}
|
||||
paddingAngle={2}
|
||||
dataKey="total"
|
||||
nameKey="tls_security"
|
||||
>
|
||||
{security.data.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={COLORS[entry.tls_security as keyof typeof COLORS] || "#8884d8"} />
|
||||
))}
|
||||
</Pie>
|
||||
<RechartsTooltip
|
||||
contentStyle={{ backgroundColor: '#161b22', borderColor: '#30363d', color: '#c9d1d9' }}
|
||||
formatter={(value: any) => [fmtBytes(value as number), 'Volume']}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="flex flex-col justify-center ml-4 space-y-2">
|
||||
{security.data.map((entry, index) => (
|
||||
<div key={`${entry.tls_security}-${index}`} className="flex items-center text-xs text-muted-foreground whitespace-nowrap">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full mr-2 shrink-0"
|
||||
style={{ backgroundColor: COLORS[entry.tls_security as keyof typeof COLORS] || "#8884d8" }}
|
||||
/>
|
||||
<span>{entry.tls_security}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* TLS Versions Bar Chart */}
|
||||
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">TLS Versions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-[250px] w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={versions.data} margin={{ top: 10, right: 10, left: 20, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#30363d" vertical={false} />
|
||||
<XAxis dataKey="tls_version" stroke="#8b949e" fontSize={10} tickLine={false} axisLine={false} />
|
||||
<YAxis
|
||||
stroke="#8b949e"
|
||||
fontSize={10}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => {
|
||||
const gb = value / 1073741824;
|
||||
const mb = value / 1048576;
|
||||
return gb >= 1 ? `${gb.toFixed(1)}GB` : `${mb.toFixed(0)}MB`;
|
||||
}}
|
||||
/>
|
||||
<RechartsTooltip
|
||||
cursor={{fill: '#21262d'}}
|
||||
contentStyle={{ backgroundColor: '#161b22', borderColor: '#30363d', color: '#c9d1d9' }}
|
||||
formatter={(value: any) => [fmtBytes(value as number), 'Volume']}
|
||||
/>
|
||||
<Bar dataKey="total" fill="#bc8cff" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{/* TLS Versions Table */}
|
||||
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">TLS Versions Overview</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={versions.data}
|
||||
columns={versionColumns}
|
||||
searchPlaceholder="Search versions..."
|
||||
searchFilter={(row, query) => (row.tls_version || "").toLowerCase().includes(query.toLowerCase())}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* TLS Ciphers Table */}
|
||||
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Top TLS Cipher Suites</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={ciphers.data}
|
||||
columns={cipherColumns}
|
||||
searchPlaceholder="Search ciphers..."
|
||||
searchFilter={(row, query) => (row.tls_cipher || "").toLowerCase().includes(query.toLowerCase())}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
||||
import { DataTable, Column } from "@/components/ui/DataTable";
|
||||
import { Badge } from "@/components/ui/Badge";
|
||||
import { useState, useEffect } from "react";
|
||||
import { ShieldCheck, Loader2 } from "lucide-react";
|
||||
import { useThreats, ThreatStat } from "@/lib/api";
|
||||
|
||||
export default function ThreatsPage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const threats = useThreats(100);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
const getSeverityBadge = (sev?: string | null) => {
|
||||
if (!sev) return <Badge variant="green">Unknown</Badge>;
|
||||
const s = sev.toLowerCase();
|
||||
if (s.includes('high') || s.includes('critical')) return <Badge variant="red">{sev}</Badge>;
|
||||
if (s.includes('med')) return <Badge variant="orange">{sev}</Badge>;
|
||||
if (s.includes('low')) return <Badge variant="yellow">{sev}</Badge>;
|
||||
return <Badge variant="green">{sev}</Badge>;
|
||||
};
|
||||
|
||||
const columns: Column<ThreatStat>[] = [
|
||||
{ header: "#", accessor: (row, i) => i + 1 },
|
||||
{ header: "Type", accessor: (row) => row.threat_type || 'Unknown' },
|
||||
{ header: "Severity", accessor: (row) => getSeverityBadge(row.severity) },
|
||||
{ header: "Source IP", accessor: (row) => <span className="font-medium text-blue-400">{row.ip_address || '-'}</span> },
|
||||
{ header: "Dest IP", accessor: (row) => row.dst_ip || '-' },
|
||||
{ header: "MAC Address", accessor: (row) => row.mac_address ? <code className="text-xs bg-secondary/50 px-1 py-0.5 rounded text-muted-foreground">{row.mac_address}</code> : '-' },
|
||||
{ header: "App", accessor: (row) => row.app_label || '-' },
|
||||
{ header: "Domain", accessor: (row) => row.domain || '-' },
|
||||
{ header: "Time", accessor: (row) => <span className="text-muted-foreground text-xs">{row.detected_at ? new Date(row.detected_at).toLocaleString() : '-'}</span> },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-in pb-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight text-white flex items-center gap-3">
|
||||
Threat Intelligence
|
||||
{threats.isLoading && <Loader2 className="w-5 h-5 animate-spin text-primary" />}
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-1">Monitor and respond to network security threats.</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 px-4 py-2 rounded-lg shadow-[0_0_15px_rgba(16,185,129,0.1)]">
|
||||
<ShieldCheck className="h-4 w-4" />
|
||||
<span>Active Monitoring</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="bg-card/80 backdrop-blur-xl border-border/50 animate-slide-up" style={{ animationDelay: '100ms' }}>
|
||||
<CardHeader className="border-b border-border/50 pb-4 mb-4">
|
||||
<CardTitle className="text-lg text-white">Detected Threats {threats.data ? `(${threats.data.length})` : ''}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={threats.data || []}
|
||||
columns={columns}
|
||||
searchPlaceholder="Search threats by IP, domain, type..."
|
||||
searchFilter={(row, query) => {
|
||||
const q = query.toLowerCase();
|
||||
return (row.ip_address || "").toLowerCase().includes(q) ||
|
||||
(row.domain || "").toLowerCase().includes(q) ||
|
||||
(row.threat_type || "").toLowerCase().includes(q) ||
|
||||
(row.app_label || "").toLowerCase().includes(q);
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, useState, useEffect } from "react";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
import { Header } from "./Header";
|
||||
|
||||
export function DashboardLayout({ children }: { children: ReactNode }) {
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [lastUpdate, setLastUpdate] = useState<string>("—");
|
||||
|
||||
useEffect(() => {
|
||||
setLastUpdate(new Date().toLocaleString('id-ID'));
|
||||
}, []);
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setIsRefreshing(true);
|
||||
// Simulate API refresh
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
setLastUpdate(new Date().toLocaleString('id-ID'));
|
||||
setIsRefreshing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-background">
|
||||
<Sidebar />
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<Header
|
||||
lastUpdate={lastUpdate}
|
||||
isRefreshing={isRefreshing}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
<main className="flex-1 overflow-y-auto bg-[#0d1117] p-6">
|
||||
<div className="mx-auto max-w-7xl space-y-6">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import { Bell, RefreshCw } from "lucide-react";
|
||||
|
||||
export function Header({
|
||||
lastUpdate = "—",
|
||||
isRefreshing = false,
|
||||
onRefresh = () => {},
|
||||
}: {
|
||||
lastUpdate?: string;
|
||||
isRefreshing?: boolean;
|
||||
onRefresh?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<header className="flex h-16 shrink-0 items-center justify-between border-b border-border bg-background px-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 rounded-full border border-border bg-card px-3 py-1 text-xs text-muted-foreground">
|
||||
<div className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
|
||||
Site: {process.env.NEXT_PUBLIC_SITE_ID || "Unknown Site"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Update: {lastUpdate}
|
||||
</div>
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
disabled={isRefreshing}
|
||||
className="flex items-center gap-2 rounded-md bg-secondary px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-secondary/80 disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||
Refresh
|
||||
</button>
|
||||
<button className="relative rounded-full p-2 text-muted-foreground hover:bg-secondary hover:text-white transition-colors">
|
||||
<Bell className="h-5 w-5" />
|
||||
<span className="absolute right-2 top-2 h-2 w-2 rounded-full bg-destructive" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Activity,
|
||||
AppWindow,
|
||||
Globe,
|
||||
LayoutDashboard,
|
||||
Shield,
|
||||
ShieldAlert,
|
||||
Server,
|
||||
Zap,
|
||||
Radio,
|
||||
Lock,
|
||||
Search
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
const navigation = [
|
||||
{ name: "Summary", href: "/", icon: LayoutDashboard },
|
||||
{ name: "Agents", href: "/agents", icon: Server },
|
||||
{ name: "Devices", href: "/devices", icon: Server },
|
||||
{ name: "Flows", href: "/flows", icon: Activity },
|
||||
{ name: "Apps", href: "/apps", icon: AppWindow },
|
||||
{ name: "App Database", href: "/lookup", icon: Search },
|
||||
{ name: "Threats", href: "/threats", icon: ShieldAlert },
|
||||
{ name: "Geography", href: "/geography", icon: Globe },
|
||||
{ name: "DNS", href: "/dns", icon: Radio },
|
||||
{ name: "Events", href: "/events", icon: Zap },
|
||||
{ name: "Network Intelligence", href: "/network-intelligence", icon: Activity },
|
||||
{ name: "Security Audit", href: "/security-audit", icon: Lock },
|
||||
{ name: "Intelligence", href: "/intelligence", icon: Shield },
|
||||
{ name: "Network Infra", href: "/network-infrastructure", icon: Server },
|
||||
{ name: "DPI Analytics", href: "/dpi-analytics", icon: Activity },
|
||||
];
|
||||
|
||||
export function Sidebar() {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-64 flex-col border-r border-border/50 bg-sidebar/80 backdrop-blur-2xl relative">
|
||||
{/* Subtle glow effect behind sidebar */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-primary/5 via-transparent to-transparent pointer-events-none" />
|
||||
|
||||
<div className="flex h-[72px] shrink-0 items-center px-6 border-b border-border/50 relative z-10">
|
||||
<div className="relative flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10 border border-primary/20 mr-3 shadow-[0_0_15px_rgba(59,130,246,0.2)]">
|
||||
<Shield className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<span className="text-lg font-bold text-white tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-white/70">
|
||||
Netify
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col overflow-y-auto relative z-10">
|
||||
<nav className="flex-1 space-y-1.5 px-3 py-6">
|
||||
{navigation.map((item) => {
|
||||
const isActive = pathname === item.href;
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"group relative flex items-center rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200 overflow-hidden",
|
||||
isActive
|
||||
? "bg-primary/10 text-white shadow-[0_0_10px_rgba(59,130,246,0.1)]"
|
||||
: "text-muted-foreground hover:bg-secondary/50 hover:text-white"
|
||||
)}
|
||||
>
|
||||
{/* Active indicator bar */}
|
||||
{isActive && (
|
||||
<div className="absolute left-0 top-0 bottom-0 w-1 bg-primary rounded-r-full shadow-[0_0_10px_rgba(59,130,246,0.8)]" />
|
||||
)}
|
||||
|
||||
<item.icon
|
||||
className={cn(
|
||||
"mr-3 h-5 w-5 shrink-0 transition-all duration-300",
|
||||
isActive
|
||||
? "text-primary drop-shadow-[0_0_8px_rgba(59,130,246,0.5)] scale-110"
|
||||
: "text-muted-foreground group-hover:text-white group-hover:scale-110"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="relative z-10">{item.name}</span>
|
||||
|
||||
{/* Hover background effect */}
|
||||
{!isActive && (
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-white/0 via-white/5 to-white/0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none translate-x-[-100%] group-hover:translate-x-[100%] transition-transform" />
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
variant?: "default" | "red" | "orange" | "yellow" | "green" | "blue" | "cyan";
|
||||
}
|
||||
|
||||
export function Badge({ className, variant = "default", ...props }: BadgeProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
"border-primary/20 bg-primary/10 text-primary": variant === "default",
|
||||
"border-red-500/20 bg-red-500/10 text-red-500": variant === "red",
|
||||
"border-orange-500/20 bg-orange-500/10 text-orange-500": variant === "orange",
|
||||
"border-yellow-500/20 bg-yellow-500/10 text-yellow-500": variant === "yellow",
|
||||
"border-emerald-500/20 bg-emerald-500/10 text-emerald-500": variant === "green",
|
||||
"border-blue-500/20 bg-blue-500/10 text-blue-500": variant === "blue",
|
||||
"border-cyan-500/20 bg-cyan-500/10 text-cyan-500": variant === "cyan",
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export function Card({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement> & {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-xl border border-border bg-card/80 backdrop-blur-xl text-card-foreground shadow-sm hover:shadow-primary/5 hover:border-primary/20 transition-all duration-300 ease-in-out hover:-translate-y-0.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CardHeader({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("flex flex-col space-y-1.5 p-6", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CardTitle({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<h3
|
||||
className={cn(
|
||||
"font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</h3>
|
||||
);
|
||||
}
|
||||
|
||||
export function CardContent({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("p-6 pt-0", className)}>{children}</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Search } from "lucide-react";
|
||||
|
||||
export interface Column<T> {
|
||||
header: string;
|
||||
accessor: (row: T, index: number) => React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface DataTableProps<T> {
|
||||
data: T[];
|
||||
columns: Column<T>[];
|
||||
searchPlaceholder?: string;
|
||||
searchFilter?: (row: T, query: string) => boolean;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function DataTable<T>({
|
||||
data,
|
||||
columns,
|
||||
searchPlaceholder = "Search...",
|
||||
searchFilter,
|
||||
isLoading
|
||||
}: DataTableProps<T>) {
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const filteredData = searchFilter
|
||||
? data.filter(row => searchFilter(row, query))
|
||||
: data;
|
||||
|
||||
return (
|
||||
<div className="space-y-4 animate-fade-in">
|
||||
{searchFilter && (
|
||||
<div className="relative group">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground transition-colors group-focus-within:text-primary" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={searchPlaceholder}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="w-full max-w-sm rounded-lg border border-border bg-card/30 backdrop-blur-sm pl-9 pr-4 py-2.5 text-sm text-white placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:bg-card/50 transition-all shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="rounded-xl border border-border overflow-hidden bg-card/50 backdrop-blur-xl shadow-sm">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="bg-secondary/30 text-muted-foreground border-b border-border/50">
|
||||
<tr>
|
||||
{columns.map((col, i) => (
|
||||
<th key={i} className={`px-4 py-3.5 font-medium whitespace-nowrap tracking-wide ${col.className || ""}`}>
|
||||
{col.header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border/50">
|
||||
{isLoading ? (
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="px-4 py-12 text-center text-muted-foreground">
|
||||
<div className="flex flex-col items-center justify-center gap-3 animate-pulse">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-primary/20 rounded-full blur-xl"></div>
|
||||
<svg className="animate-spin relative h-8 w-8 text-primary" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-sm font-medium tracking-wide">Loading data...</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : filteredData.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="px-4 py-12 text-center">
|
||||
<div className="flex flex-col items-center justify-center text-muted-foreground">
|
||||
<Search className="h-8 w-8 mb-3 opacity-20" />
|
||||
<span className="text-sm font-medium">No data found</span>
|
||||
{query && <span className="text-xs mt-1 opacity-70">Try adjusting your search query</span>}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredData.map((row, i) => (
|
||||
<tr key={i} className="hover:bg-primary/5 transition-colors duration-200 group">
|
||||
{columns.map((col, j) => (
|
||||
<td key={j} className={`px-4 py-3 whitespace-nowrap text-foreground group-hover:text-white transition-colors ${col.className || ""}`}>
|
||||
{col.accessor(row, i)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { X } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Modal({ isOpen, onClose, title, children, className }: ModalProps) {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
document.body.style.overflow = "unset";
|
||||
}
|
||||
return () => {
|
||||
document.body.style.overflow = "unset";
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
if (!isMounted || !isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-0">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="fixed inset-0 bg-black/60 backdrop-blur-sm transition-opacity"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Dialog */}
|
||||
<div className={cn(
|
||||
"relative z-50 w-full max-w-md transform overflow-hidden rounded-xl border border-border bg-card shadow-2xl transition-all sm:my-8",
|
||||
className
|
||||
)}>
|
||||
<div className="flex items-center justify-between border-b border-border px-6 py-4">
|
||||
<h3 className="text-lg font-semibold text-white tracking-tight">{title}</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="rounded-md p-1 text-muted-foreground hover:bg-secondary hover:text-white transition-colors focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-6 py-4">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface TabProps {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Tab({ children }: TabProps) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
interface TabsProps {
|
||||
children: React.ReactElement<TabProps>[];
|
||||
}
|
||||
|
||||
export function Tabs({ children }: TabsProps) {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex space-x-1 border-b border-white/10 overflow-x-auto pb-1">
|
||||
{React.Children.map(children, (child, index) => {
|
||||
if (!React.isValidElement(child)) return null;
|
||||
const isActive = index === activeTab;
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setActiveTab(index)}
|
||||
className={`
|
||||
px-4 py-2 text-sm font-medium rounded-t-lg transition-colors whitespace-nowrap
|
||||
${isActive
|
||||
? 'bg-blue-600/20 text-blue-400 border-b-2 border-blue-500'
|
||||
: 'text-gray-400 hover:text-gray-200 hover:bg-white/5'}
|
||||
`}
|
||||
>
|
||||
{child.props.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
{React.Children.map(children, (child, index) => {
|
||||
if (index !== activeTab) return null;
|
||||
return child;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
|
||||
import React, { memo, useState, useEffect } from "react";
|
||||
import {
|
||||
ComposableMap,
|
||||
Geographies,
|
||||
Geography,
|
||||
Sphere,
|
||||
Graticule,
|
||||
ZoomableGroup
|
||||
} from "react-simple-maps";
|
||||
import { scaleLinear } from "d3-scale";
|
||||
import { fmtBytes } from "@/lib/utils";
|
||||
|
||||
// The topojson file containing the world shapes
|
||||
const geoUrl = "https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json";
|
||||
|
||||
// Our custom heatmap color scale
|
||||
// Maps values from 0 to maxValue to a gradient (Dark Blue to Electric Cyan/Purple)
|
||||
const colorScale = scaleLinear<string>()
|
||||
.domain([0, 100]) // We will update the domain dynamically based on data
|
||||
.range(["#1e293b", "#38bdf8"]); // Tailwind slate-800 to sky-400
|
||||
|
||||
interface WorldMapProps {
|
||||
data: {
|
||||
countryCode: string; // ISO A2 code
|
||||
value: number; // The value used for heatmap
|
||||
label: string; // Country name for tooltip
|
||||
}[];
|
||||
}
|
||||
|
||||
const WorldMapComponent = ({ data }: WorldMapProps) => {
|
||||
const [tooltipContent, setTooltipContent] = useState("");
|
||||
const [maxValue, setMaxValue] = useState(0);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (data.length > 0) {
|
||||
const max = Math.max(...data.map(d => d.value));
|
||||
setMaxValue(max);
|
||||
colorScale.domain([0, max]);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
return (
|
||||
<div className="relative w-full aspect-[2/1] bg-secondary/20 rounded-xl overflow-hidden border border-border/50 backdrop-blur-sm">
|
||||
<div className="absolute top-4 left-4 z-10 flex flex-col gap-1 pointer-events-none bg-card/80 p-3 rounded-lg border border-border/50 backdrop-blur-md">
|
||||
<div className="text-sm font-medium text-white flex items-center gap-2">
|
||||
Heatmap Activity
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<div className="text-xs text-muted-foreground">Low</div>
|
||||
<div className="w-24 h-2 rounded-full bg-gradient-to-r from-slate-800 to-sky-400 shadow-sm"></div>
|
||||
<div className="text-xs text-muted-foreground">High</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tooltip Overlay */}
|
||||
{tooltipContent && (
|
||||
<div className="absolute bottom-4 left-4 z-10 bg-card/90 backdrop-blur-md border border-border/50 px-4 py-2.5 rounded-lg shadow-lg pointer-events-none animate-fade-in">
|
||||
<div className="text-sm font-semibold text-white">{tooltipContent.split(':')[0]}</div>
|
||||
<div className="text-xs text-sky-400 mt-1 font-medium">{tooltipContent.split(':')[1]}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ComposableMap
|
||||
projection="geoMercator"
|
||||
projectionConfig={{
|
||||
scale: 130,
|
||||
center: [0, 30]
|
||||
}}
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-full h-full"
|
||||
>
|
||||
<ZoomableGroup zoom={1} maxZoom={4} translateExtent={[[-100, -100], [900, 500]]}>
|
||||
<Sphere stroke="rgba(255,255,255,0.05)" strokeWidth={0.5} id="sphere" fill="transparent" />
|
||||
<Graticule stroke="rgba(255,255,255,0.05)" strokeWidth={0.5} />
|
||||
|
||||
<Geographies geography={geoUrl}>
|
||||
{({ geographies }) =>
|
||||
geographies.map((geo) => {
|
||||
// geo.properties.name contains the country name in this TopoJSON
|
||||
// Try to find if we have data for this country
|
||||
const d = data.find((s) => {
|
||||
if (!geo.properties.name) return false;
|
||||
const geoName = geo.properties.name.toLowerCase();
|
||||
const sLabel = s.label.toLowerCase();
|
||||
|
||||
return geoName === sLabel ||
|
||||
(sLabel === 'united states' && geoName === 'united states of america') ||
|
||||
(sLabel === 'the netherlands' && geoName === 'netherlands') ||
|
||||
(sLabel === 'south korea' && geoName === 'korea');
|
||||
});
|
||||
|
||||
const color = d ? colorScale(d.value) : "#1e293b"; // Default color
|
||||
|
||||
return (
|
||||
<Geography
|
||||
key={geo.rsmKey}
|
||||
geography={geo}
|
||||
fill={color}
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth={0.5}
|
||||
style={{
|
||||
default: { outline: "none" },
|
||||
hover: {
|
||||
fill: "#0ea5e9", // sky-500 on hover
|
||||
outline: "none",
|
||||
transition: "all 200ms ease"
|
||||
},
|
||||
pressed: { outline: "none" }
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (d) {
|
||||
setTooltipContent(`${d.label}: ${fmtBytes(d.value)}`);
|
||||
} else {
|
||||
setTooltipContent(`${geo.properties.name}: No data`);
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setTooltipContent("");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Geographies>
|
||||
</ZoomableGroup>
|
||||
</ComposableMap>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const WorldMap = memo(WorldMapComponent);
|
||||
@@ -0,0 +1,152 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
const API_URL = process.env.NETIFY_BASE_URL ? `${process.env.NETIFY_BASE_URL}/assets/agents` : "https://manager.netify.ai/api/v1/assets/agents";
|
||||
|
||||
// Helper function to get headers
|
||||
const getHeaders = () => {
|
||||
const API_KEY = process.env.NETIFY_API_KEY;
|
||||
const JWT_TOKEN = process.env.NETIFY_JWT_TOKEN;
|
||||
const SITE_UUID = process.env.NETIFY_SITE_UUID;
|
||||
|
||||
if (!API_KEY && !JWT_TOKEN) {
|
||||
throw new Error("NETIFY_API_KEY or NETIFY_JWT_TOKEN is not set in environment variables");
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
// JWT Token access requires x-net-site
|
||||
if (JWT_TOKEN && SITE_UUID) {
|
||||
headers["x-api-key"] = JWT_TOKEN;
|
||||
headers["x-net-site"] = SITE_UUID;
|
||||
} else if (API_KEY) {
|
||||
headers["x-api-key"] = API_KEY;
|
||||
}
|
||||
|
||||
return headers;
|
||||
};
|
||||
|
||||
export interface Agent {
|
||||
id: number;
|
||||
uuid: string;
|
||||
serial: string;
|
||||
site_uuid: string;
|
||||
organization_uuid: string;
|
||||
provisioned: boolean;
|
||||
activated: boolean;
|
||||
label?: string;
|
||||
created_at: { human: string; date: string; unix_time: number };
|
||||
updated_at: { human: string; date: string; unix_time: number };
|
||||
last_seen_at: { human: string; date: string; unix_time: number };
|
||||
}
|
||||
|
||||
export async function getAgents(): Promise<Agent[]> {
|
||||
try {
|
||||
const response = await fetch(API_URL, {
|
||||
method: "GET",
|
||||
headers: getHeaders(),
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 403) {
|
||||
throw new Error("Asset Management Disabled: The configured API key does not have permission to manage provisioning agents (403 Forbidden).");
|
||||
}
|
||||
throw new Error(`Failed to fetch agents: ${response.statusText} (${response.status})`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (result.status_code !== 0) {
|
||||
throw new Error(result.status_message || "Unknown API error");
|
||||
}
|
||||
|
||||
return result.data || [];
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching agents:", error);
|
||||
throw new Error(error.message || "Failed to retrieve agents inventory.");
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAgent(agentId: string): Promise<Agent | null> {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/${agentId}`, {
|
||||
method: "GET",
|
||||
headers: getHeaders(),
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch agent ${agentId}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching agent ${agentId}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createAgent(agentId: string, label: string) {
|
||||
try {
|
||||
const url = agentId
|
||||
? `${API_URL}/${encodeURIComponent(agentId)}?label=${encodeURIComponent(label)}`
|
||||
: `${API_URL}?label=${encodeURIComponent(label)}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: getHeaders(),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (!response.ok || result.status_code !== 0) {
|
||||
return { success: false, message: result.status_message || response.statusText };
|
||||
}
|
||||
|
||||
revalidatePath('/agents');
|
||||
return { success: true, data: result.data };
|
||||
} catch (error: any) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateAgent(agentId: string, label: string) {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/${encodeURIComponent(agentId)}?label=${encodeURIComponent(label)}`, {
|
||||
method: "PATCH",
|
||||
headers: getHeaders(),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (!response.ok || result.status_code !== 0) {
|
||||
return { success: false, message: result.status_message || response.statusText };
|
||||
}
|
||||
|
||||
revalidatePath('/agents');
|
||||
return { success: true, data: result.data };
|
||||
} catch (error: any) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteAgent(agentId: string) {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/${encodeURIComponent(agentId)}`, {
|
||||
method: "DELETE",
|
||||
headers: getHeaders(),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (!response.ok || result.status_code !== 0) {
|
||||
return { success: false, message: result.status_message || response.statusText };
|
||||
}
|
||||
|
||||
revalidatePath('/agents');
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { fetcher } from './api';
|
||||
|
||||
// --- ADVANCED NETWORK INFRASTRUCTURE TYPES ---
|
||||
export interface RegionStat { id: number; fetched_at: string; region_name: string; region_code: string; country_name: string; country_code: string; download: number; }
|
||||
export interface CityStat { id: number; fetched_at: string; city_name: string; region_name: string; country_name: string; country_code: string; download: number; }
|
||||
export interface VlanStat { id: number; fetched_at: string; vlan_id: number; vlan_label: string; download: number; upload: number; total: number; }
|
||||
export interface InterfaceStat { id: number; fetched_at: string; iface_id: number; iface_name: string; iface_role: string; agent_id: string; download: number; upload: number; total: number; }
|
||||
export interface IpVersionStat { id: number; fetched_at: string; ip_version_label: string; download: number; upload: number; total: number; }
|
||||
export interface RemoteIpStat { id: number; fetched_at: string; remote_ip: string; ip_version: number; download: number; upload: number; total: number; }
|
||||
export interface MacBandwidthStat { id: number; fetched_at: string; mac_address: string; manufacturer: string; download: number; upload: number; total: number; }
|
||||
export interface FlowTypeStat { id: number; fetched_at: string; flow_type_label: string; download: number; upload: number; total: number; }
|
||||
export interface FlowOriginStat { id: number; fetched_at: string; flow_origin_label: string; download: number; upload: number; total: number; }
|
||||
|
||||
// --- DPI ANALYTICS TYPES ---
|
||||
export interface NetbiosStat { id: number; fetched_at: string; hostname: string; download: number; upload: number; total: number; }
|
||||
export interface DiscoveryOsStat { id: number; fetched_at: string; os_label: string; download: number; upload: number; total: number; }
|
||||
export interface DhcpFingerprintStat { id: number; fetched_at: string; fingerprint: string; download: number; upload: number; total: number; }
|
||||
export interface HttpUserAgentStat { id: number; fetched_at: string; user_agent: string; download: number; upload: number; total: number; }
|
||||
export interface SniHostnameStat { id: number; fetched_at: string; sni_hostname: string; download: number; upload: number; total: number; }
|
||||
export interface SslServerCnStat { id: number; fetched_at: string; ssl_server_cn: string; download: number; upload: number; total: number; }
|
||||
export interface QuicHostnameStat { id: number; fetched_at: string; quic_hostname: string; download: number; upload: number; total: number; }
|
||||
export interface BittorrentHashStat { id: number; fetched_at: string; info_hash: string; label: string; download: number; upload: number; total: number; }
|
||||
export interface SshVersionStat { id: number; fetched_at: string; ssh_version: string; download: number; upload: number; total: number; }
|
||||
export interface MdnsHostnameStat { id: number; fetched_at: string; mdns_hostname: string; download: number; upload: number; total: number; }
|
||||
|
||||
// --- INTELLIGENCE DETAIL TYPES ---
|
||||
export interface IntelCryptoMining { id: number; fetched_at: string; detected_at: string; ip_address: string; mac_address: string; pool_host: string; pool_ip: string; protocol: string; app_label: string; confidence: number; download: number; upload: number; }
|
||||
export interface IntelDeviceDiscovery { id: number; fetched_at: string; detected_at: string; ip_address: string; mac_address: string; device_label: string; device_type: string; os_label: string; manufacturer: string; is_new: number; }
|
||||
export interface IntelEncryptionAudit { id: number; fetched_at: string; detected_at: string; ip_address: string; mac_address: string; device_label: string; encrypted_pct: number; unencrypted: number; encrypted: number; total: number; risk_level: string; }
|
||||
export interface IntelInsecureProtocol { id: number; fetched_at: string; detected_at: string; protocol: string; ip_address: string; mac_address: string; dst_ip: string; dst_port: number; app_label: string; download: number; upload: number; risk: string; source: string; }
|
||||
export interface IntelIpReputation { id: number; fetched_at: string; detected_at: string; ip_address: string; local_ip: string; mac_address: string; reputation: string; score: number; country: string; app_label: string; download: number; upload: number; blacklisted: number; }
|
||||
export interface IntelServerDiscovery { id: number; fetched_at: string; detected_at: string; ip_address: string; mac_address: string; server_type: string; hostname: string; port: number; protocol: string; os_label: string; download: number; upload: number; }
|
||||
export interface IntelTorDetection { id: number; fetched_at: string; detected_at: string; ip_address: string; mac_address: string; exit_node: string; circuit_id: string; download: number; upload: number; country: string; }
|
||||
export interface IntelUnencryptedPassword { id: number; fetched_at: string; detected_at: string; ip_address: string; mac_address: string; dst_ip: string; dst_port: number; protocol: string; username: string; download: number; upload: number; severity: string; }
|
||||
export interface IntelVpnDetection { id: number; fetched_at: string; detected_at: string; ip_address: string; mac_address: string; vpn_type: string; remote_ip: string; protocol: string; download: number; upload: number; country: string; confidence: number; }
|
||||
|
||||
function useGenericFetch<T>(endpoint: string, limit: number) {
|
||||
const [data, setData] = useState<T[]>([]);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetcher<T[]>(`${endpoint}?limit=${limit}`)
|
||||
.then((res) => setData(res || []))
|
||||
.catch(setError)
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [endpoint, limit]);
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
|
||||
// Network Infrastructure Hooks
|
||||
export const useRegions = (limit = 50) => useGenericFetch<RegionStat>('/regions', limit);
|
||||
export const useCities = (limit = 50) => useGenericFetch<CityStat>('/cities', limit);
|
||||
export const useVlans = (limit = 50) => useGenericFetch<VlanStat>('/vlans', limit);
|
||||
export const useInterfaces = (limit = 50) => useGenericFetch<InterfaceStat>('/interfaces', limit);
|
||||
export const useIpVersions = (limit = 50) => useGenericFetch<IpVersionStat>('/ip-versions', limit);
|
||||
export const useRemoteIps = (limit = 50) => useGenericFetch<RemoteIpStat>('/remote-ips', limit);
|
||||
export const useMacBandwidth = (limit = 50) => useGenericFetch<MacBandwidthStat>('/mac-bandwidth', limit);
|
||||
export const useFlowTypes = (limit = 50) => useGenericFetch<FlowTypeStat>('/flow-types', limit);
|
||||
export const useFlowOrigins = (limit = 50) => useGenericFetch<FlowOriginStat>('/flow-origins', limit);
|
||||
|
||||
// DPI Analytics Hooks
|
||||
export const useNetbios = (limit = 50) => useGenericFetch<NetbiosStat>('/netbios', limit);
|
||||
export const useDiscoveryOs = (limit = 50) => useGenericFetch<DiscoveryOsStat>('/discovery-os', limit);
|
||||
export const useDhcpFingerprints = (limit = 50) => useGenericFetch<DhcpFingerprintStat>('/dhcp-fingerprints', limit);
|
||||
export const useHttpUserAgents = (limit = 50) => useGenericFetch<HttpUserAgentStat>('/http-user-agents', limit);
|
||||
export const useSniHostnames = (limit = 50) => useGenericFetch<SniHostnameStat>('/sni-hostnames', limit);
|
||||
export const useSslServerCn = (limit = 50) => useGenericFetch<SslServerCnStat>('/ssl-server-cn', limit);
|
||||
export const useQuicHostnames = (limit = 50) => useGenericFetch<QuicHostnameStat>('/quic-hostnames', limit);
|
||||
export const useBittorrentHashes = (limit = 50) => useGenericFetch<BittorrentHashStat>('/bittorrent-hashes', limit);
|
||||
export const useSshVersions = (limit = 50) => useGenericFetch<SshVersionStat>('/ssh-versions', limit);
|
||||
export const useMdnsHostnames = (limit = 50) => useGenericFetch<MdnsHostnameStat>('/mdns-hostnames', limit);
|
||||
|
||||
// Intelligence Detail Hooks
|
||||
export const useIntelCryptoMining = (limit = 50) => useGenericFetch<IntelCryptoMining>('/intelligence/crypto-mining', limit);
|
||||
export const useIntelDeviceDiscovery = (limit = 50) => useGenericFetch<IntelDeviceDiscovery>('/intelligence/device-discovery', limit);
|
||||
export const useIntelEncryptionAudit = (limit = 50) => useGenericFetch<IntelEncryptionAudit>('/intelligence/encryption-audit', limit);
|
||||
export const useIntelInsecureProtocols = (limit = 50) => useGenericFetch<IntelInsecureProtocol>('/intelligence/insecure-protocols', limit);
|
||||
export const useIntelIpReputation = (limit = 50) => useGenericFetch<IntelIpReputation>('/intelligence/ip-reputation', limit);
|
||||
export const useIntelServerDiscovery = (limit = 50) => useGenericFetch<IntelServerDiscovery>('/intelligence/server-discovery', limit);
|
||||
export const useIntelTorDetection = (limit = 50) => useGenericFetch<IntelTorDetection>('/intelligence/tor', limit);
|
||||
export const useIntelUnencryptedPasswords = (limit = 50) => useGenericFetch<IntelUnencryptedPassword>('/intelligence/unencrypted-passwords', limit);
|
||||
export const useIntelVpnDetection = (limit = 50) => useGenericFetch<IntelVpnDetection>('/intelligence/vpn', limit);
|
||||
+460
@@ -0,0 +1,460 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
// Common Response Wrapper
|
||||
export interface ApiResponse<T> {
|
||||
ok: boolean;
|
||||
data: T;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// Data Types
|
||||
export interface FlowStat {
|
||||
id: number;
|
||||
fetched_at: string;
|
||||
flow_id: string;
|
||||
src_ip: string;
|
||||
src_mac: string | null;
|
||||
dst_ip: string;
|
||||
dst_port: number;
|
||||
protocol: string;
|
||||
app_label: string | null;
|
||||
domain: string | null;
|
||||
bytes_download: number;
|
||||
bytes_upload: number;
|
||||
first_seen: string;
|
||||
last_seen: string;
|
||||
}
|
||||
|
||||
export interface CountryStat {
|
||||
id: number;
|
||||
country_code: string;
|
||||
country_name: string;
|
||||
download: number;
|
||||
upload: number;
|
||||
flow_count: number;
|
||||
}
|
||||
|
||||
export interface DnsStat {
|
||||
id: number;
|
||||
domain: string;
|
||||
query_count: number;
|
||||
app_label: string | null;
|
||||
category: string | null;
|
||||
}
|
||||
|
||||
export interface EventStat {
|
||||
id: number;
|
||||
event_type: string;
|
||||
severity: string;
|
||||
message: string;
|
||||
source_ip: string | null;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface TlsVersionStat {
|
||||
id: number;
|
||||
tls_version: string;
|
||||
download: number;
|
||||
upload: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface TlsCipherStat {
|
||||
id: number;
|
||||
tls_cipher: string;
|
||||
download: number;
|
||||
upload: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface TlsSecurityStat {
|
||||
id: number;
|
||||
tls_security: string;
|
||||
color: string;
|
||||
download: number;
|
||||
upload: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface IntelligenceStats {
|
||||
intel_crypto_mining: number;
|
||||
intel_device_discovery: number;
|
||||
intel_encryption_audit: number;
|
||||
intel_insecure_protocols: number;
|
||||
intel_ip_reputation: number;
|
||||
intel_server_discovery: number;
|
||||
intel_tor_detection: number;
|
||||
intel_unencrypted_passwords: number;
|
||||
intel_vpn_detection: number;
|
||||
}
|
||||
|
||||
export interface DashboardSummary {
|
||||
total_devices: number;
|
||||
total_threats: number;
|
||||
total_events: number;
|
||||
last_fetch: string;
|
||||
bandwidth_down: number;
|
||||
bandwidth_up: number;
|
||||
active_flows: number;
|
||||
}
|
||||
|
||||
export interface TimelinePoint {
|
||||
fetched_at: string;
|
||||
total_download: number;
|
||||
total_upload: number;
|
||||
}
|
||||
|
||||
export interface AppStat {
|
||||
app_label: string;
|
||||
download: number;
|
||||
upload: number;
|
||||
favicon?: string;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
export interface ProtocolStat {
|
||||
protocol_label: string;
|
||||
download: number;
|
||||
upload: number;
|
||||
}
|
||||
|
||||
export interface DeviceStat {
|
||||
id: number;
|
||||
ip_address: string;
|
||||
mac_address: string | null;
|
||||
device_label: string | null;
|
||||
device_type: string | null;
|
||||
os_label: string | null;
|
||||
manufacturer: string | null;
|
||||
download: number;
|
||||
upload: number;
|
||||
last_seen: string;
|
||||
}
|
||||
|
||||
export interface ThreatStat {
|
||||
id: number;
|
||||
threat_type: string | null;
|
||||
severity: string | null;
|
||||
ip_address: string | null;
|
||||
dst_ip: string | null;
|
||||
mac_address: string | null;
|
||||
app_label: string | null;
|
||||
domain: string | null;
|
||||
detected_at: string | null;
|
||||
}
|
||||
|
||||
export interface AppCategoryStat {
|
||||
id: number;
|
||||
category_label: string;
|
||||
download: number;
|
||||
upload: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ContinentStat {
|
||||
id: number;
|
||||
continent_name: string;
|
||||
download: number;
|
||||
upload: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
// Fetcher Function
|
||||
export async function fetcher<T>(endpoint: string): Promise<T> {
|
||||
const url = `/api/dashboard${endpoint}`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
throw new Error(`API error: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
const json = await res.json();
|
||||
if (!json.ok) {
|
||||
throw new Error(json.message || 'API responded with ok: false');
|
||||
}
|
||||
return json.data;
|
||||
}
|
||||
|
||||
// Custom Hooks for React Components
|
||||
export function useDashboardSummary() {
|
||||
const [data, setData] = useState<DashboardSummary | null>(null);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetcher<DashboardSummary>('/summary')
|
||||
.then(setData)
|
||||
.catch(setError)
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
|
||||
export function useTimeline(points: number = 60) {
|
||||
const [data, setData] = useState<TimelinePoint[]>([]);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetcher<TimelinePoint[]>(`/timeline?points=${points}`)
|
||||
.then((res) => {
|
||||
// Reverse array if backend sends newest first, so oldest is first for the chart
|
||||
setData(Array.isArray(res) ? res.slice().reverse() : []);
|
||||
})
|
||||
.catch(setError)
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [points]);
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
|
||||
export function useTopApps(limit: number = 10) {
|
||||
const [data, setData] = useState<AppStat[]>([]);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetcher<AppStat[]>(`/apps?limit=${limit}`)
|
||||
.then((res) => setData(res || []))
|
||||
.catch(setError)
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [limit]);
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
|
||||
export function useTopProtocols() {
|
||||
const [data, setData] = useState<ProtocolStat[]>([]);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetcher<ProtocolStat[]>('/protocols')
|
||||
.then((res) => setData(res || []))
|
||||
.catch(setError)
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
|
||||
export function useDevices(limit: number = 50) {
|
||||
const [data, setData] = useState<DeviceStat[]>([]);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetcher<DeviceStat[]>(`/devices?limit=${limit}`)
|
||||
.then((res) => setData(res || []))
|
||||
.catch(setError)
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [limit]);
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
|
||||
export function useThreats(limit: number = 20) {
|
||||
const [data, setData] = useState<ThreatStat[]>([]);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetcher<ThreatStat[]>(`/threats?limit=${limit}`)
|
||||
.then((res) => setData(res || []))
|
||||
.catch(setError)
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [limit]);
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
|
||||
export function useAppCategories() {
|
||||
const [data, setData] = useState<AppCategoryStat[]>([]);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetcher<AppCategoryStat[]>('/app-categories')
|
||||
.then((res) => setData(res || []))
|
||||
.catch(setError)
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
|
||||
export function useContinents() {
|
||||
const [data, setData] = useState<ContinentStat[]>([]);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetcher<ContinentStat[]>('/continents')
|
||||
.then((res) => setData(res || []))
|
||||
.catch(setError)
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
|
||||
export function useFlows(limit: number = 50) {
|
||||
const [data, setData] = useState<FlowStat[]>([]);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetcher<FlowStat[]>(`/flows?limit=${limit}`)
|
||||
.then((res) => setData(res || []))
|
||||
.catch(setError)
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [limit]);
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
|
||||
export function useCountries(limit: number = 50) {
|
||||
const [data, setData] = useState<CountryStat[]>([]);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetcher<CountryStat[]>(`/countries?limit=${limit}`)
|
||||
.then((res) => setData(res || []))
|
||||
.catch(setError)
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [limit]);
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
|
||||
export function useDNS(limit: number = 50) {
|
||||
const [data, setData] = useState<DnsStat[]>([]);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetcher<DnsStat[]>(`/dns?limit=${limit}`)
|
||||
.then((res) => setData(res || []))
|
||||
.catch(setError)
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [limit]);
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
|
||||
export function useEvents(limit: number = 50) {
|
||||
const [data, setData] = useState<EventStat[]>([]);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetcher<EventStat[]>(`/events?limit=${limit}`)
|
||||
.then((res) => setData(res || []))
|
||||
.catch(setError)
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [limit]);
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
|
||||
export function useTlsVersions(limit: number = 10) {
|
||||
const [data, setData] = useState<TlsVersionStat[]>([]);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetcher<TlsVersionStat[]>(`/tls-versions?limit=${limit}`)
|
||||
.then((res) => setData(res || []))
|
||||
.catch(setError)
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [limit]);
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
|
||||
export function useTlsCiphers(limit: number = 15) {
|
||||
const [data, setData] = useState<TlsCipherStat[]>([]);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetcher<TlsCipherStat[]>(`/tls-ciphers?limit=${limit}`)
|
||||
.then((res) => setData(res || []))
|
||||
.catch(setError)
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [limit]);
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
|
||||
export function useTlsSecurity(limit: number = 10) {
|
||||
const [data, setData] = useState<TlsSecurityStat[]>([]);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetcher<TlsSecurityStat[]>(`/tls-security?limit=${limit}`)
|
||||
.then((res) => setData(res || []))
|
||||
.catch(setError)
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [limit]);
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
|
||||
export function useIntelligenceStats() {
|
||||
const [data, setData] = useState<IntelligenceStats | null>(null);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetcher<IntelligenceStats>('/intelligence/stats')
|
||||
.then(setData)
|
||||
.catch(setError)
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
|
||||
// ─── LOOKUP APPLICATIONS ──────────────────────────────────────────────────────
|
||||
export interface LookupApp {
|
||||
id: number;
|
||||
tag: string;
|
||||
label: string;
|
||||
name: string;
|
||||
full_name: string;
|
||||
description: string;
|
||||
favicon: string;
|
||||
icon: string;
|
||||
logo: string;
|
||||
application_category: { id: number; tag: string; label: string };
|
||||
}
|
||||
|
||||
export interface LookupApplicationsResponse {
|
||||
applications: LookupApp[];
|
||||
pagination: {
|
||||
total_records: number;
|
||||
total_pages: number;
|
||||
current_page: number;
|
||||
start: number;
|
||||
length: number;
|
||||
limit: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function useLookupApplications(page: number = 1, limit: number = 25, search: string = '') {
|
||||
const [data, setData] = useState<LookupApplicationsResponse | null>(null);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
fetcher<LookupApplicationsResponse>(`/lookup/applications?page=${page}&limit=${limit}&search=${encodeURIComponent(search)}`)
|
||||
.then(setData)
|
||||
.catch(setError)
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [page, limit, search]);
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function fmtBytes(bytes?: number) {
|
||||
if (!bytes || bytes === 0) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i];
|
||||
}
|
||||
Reference in new issue
Block a user