mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 16:37:04 +00:00
feat(companion): add auth, waf, system, exposure, wireguard modules — full AUTH→MESH spectrum
Proves the drop-a-folder flow (core untouched) and gives the dashboard one card per canonical group. Reads use known box routes; a few write/detail routes are marked TODO(api). Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
This commit is contained in:
parent
c6457d8009
commit
597446ed29
11
secubox-companion/www/modules/auth/module.json
Normal file
11
secubox-companion/www/modules/auth/module.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"id": "auth",
|
||||
"name": "Auth",
|
||||
"module": "AUTH",
|
||||
"icon": "🔑",
|
||||
"description": "Identities, sessions, access.",
|
||||
"api_base": "/api/v1/auth",
|
||||
"status_endpoint": "/api/v1/auth/status",
|
||||
"capabilities": ["read"],
|
||||
"routes": [{ "path": "", "label": "Identities" }]
|
||||
}
|
||||
34
secubox-companion/www/modules/auth/view.js
Normal file
34
secubox-companion/www/modules/auth/view.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
// SecuBox Companion :: auth — identities & sessions (read).
|
||||
export default async function mount(ctx) {
|
||||
const { root, api, base, ui } = ctx;
|
||||
const { el, esc, ago, clear } = ui;
|
||||
|
||||
const host = clear(root);
|
||||
host.append(el('p.muted', { text: 'Loading identities…' }));
|
||||
try {
|
||||
const users = await api.get(`${base}/users`).catch(() => ({ users: [] })); // TODO(api): confirm users route
|
||||
const list = users.users || users.items || [];
|
||||
clear(host);
|
||||
const card = el('div.card', {}, [el('h3', { text: `Users (${list.length})` })]);
|
||||
if (!list.length) card.append(el('div.empty', { text: 'No users listed (auth users route may be admin-gated — TODO(api)).' }));
|
||||
for (const u of list) card.append(el('div.item', {}, [
|
||||
el('div.meta', {}, [
|
||||
el('b', { text: esc(u.username || u.name || u.id) }),
|
||||
el('span', { text: [u.role, u.admin && 'admin', u.totp && '2FA'].filter(Boolean).join(' · ') }),
|
||||
]),
|
||||
u.master ? el('span.badge.ok', { text: 'master' }) : null,
|
||||
]));
|
||||
host.append(card);
|
||||
|
||||
const sess = await api.get(`${base}/sessions`).catch(() => null); // TODO(api): confirm sessions route
|
||||
if (sess) {
|
||||
const items = sess.sessions || sess.items || [];
|
||||
const sc = el('div.card', {}, [el('h3', { text: `Active sessions (${items.length})` })]);
|
||||
for (const s of items.slice(0, 30)) sc.append(el('div.item', {}, [
|
||||
el('div.meta', {}, [el('b', { text: esc(s.user || s.username || '?') }), el('span', { text: `${esc(s.ip || '')} · ${ago(s.created_at || s.ts)}` })]),
|
||||
]));
|
||||
host.append(sc);
|
||||
}
|
||||
} catch (e) { clear(host).append(el('div.empty', { text: 'Failed: ' + esc(e.message) })); }
|
||||
}
|
||||
11
secubox-companion/www/modules/exposure/module.json
Normal file
11
secubox-companion/www/modules/exposure/module.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"id": "exposure",
|
||||
"name": "Exposure",
|
||||
"module": "ROOT",
|
||||
"icon": "🚀",
|
||||
"description": "Punk Exposure: peek / poke / emancipate services (Tor · DNS · Mesh).",
|
||||
"api_base": "/api/v1/exposure",
|
||||
"status_endpoint": "/api/v1/exposure/status",
|
||||
"capabilities": ["read", "write"],
|
||||
"routes": [{ "path": "", "label": "Services" }]
|
||||
}
|
||||
47
secubox-companion/www/modules/exposure/view.js
Normal file
47
secubox-companion/www/modules/exposure/view.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
// SecuBox Companion :: exposure — Punk Exposure (peek/poke/emancipate).
|
||||
// Emancipate/revoke are outward-facing → confirm() before firing; the box
|
||||
// journals every decision (OPAD).
|
||||
export default async function mount(ctx) {
|
||||
const { root, api, base, ui } = ctx;
|
||||
const { el, esc, toast, clear, confirmAction } = ui;
|
||||
|
||||
async function load() {
|
||||
const host = clear(root);
|
||||
host.append(el('p.muted', { text: 'Peeking at services…' }));
|
||||
try {
|
||||
const d = await api.get(`${base}/exposure`).catch(() => api.get(`${base}/services`)); // /api/v1/exposure/exposure (double-segment)
|
||||
const svcs = d.services || d.exposure || d.items || (Array.isArray(d) ? d : []);
|
||||
clear(host);
|
||||
if (d.__cached) host.append(el('div.badge.warn', { text: 'cached (offline)' }));
|
||||
if (!svcs.length) return host.append(el('div.empty', { text: 'No eligible services (only LAN/0.0.0.0 services are exposable).' }));
|
||||
for (const s of svcs) {
|
||||
const on = !!(s.exposed || s.tor || s.dns || s.mesh);
|
||||
host.append(el('div.item', {}, [
|
||||
el('div.meta', {}, [
|
||||
el('b', { text: `${esc(s.name || s.service)} :${s.port ?? '?'}` }),
|
||||
el('span', { text: [s.tor && 'Tor', s.dns && 'DNS', s.mesh && 'Mesh'].filter(Boolean).join(' · ') || (s.domain || 'not exposed') }),
|
||||
]),
|
||||
el('div.actions', {}, [
|
||||
el(`button.btn.sm${on ? '.danger' : '.primary'}`, {
|
||||
text: on ? 'Revoke' : 'Emancipate',
|
||||
onclick: () => on ? act('revoke', s) : act('emancipate', s),
|
||||
}),
|
||||
]),
|
||||
]));
|
||||
}
|
||||
} catch (e) { clear(host).append(el('div.empty', { text: 'Failed: ' + esc(e.message) })); }
|
||||
}
|
||||
|
||||
async function act(verb, s) {
|
||||
if (!confirmAction(`${verb} "${s.name || s.service}" (port ${s.port})? This changes the box's external exposure.`)) return;
|
||||
try {
|
||||
// TODO(api): confirm exact emancipate/revoke route + payload
|
||||
const r = await api.post(`${base}/${verb}`, { service: s.name || s.service, port: s.port, all: true });
|
||||
toast(r.queued ? `${verb} queued (offline)` : `${verb} ✓`);
|
||||
load();
|
||||
} catch (e) { toast(`${verb} failed: ` + e.message, 'err'); }
|
||||
}
|
||||
|
||||
load();
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"_comment": "The module registry. Add a module: drop a folder in modules/<id>/ (module.json + view.js) and append its id here. The core never changes.",
|
||||
"modules": ["billets", "podcasteur"]
|
||||
"_comment": "The module registry. Add a module: drop a folder in modules/<id>/ (module.json + view.js) and append its id here. The core never changes. The dashboard orders them AUTH→WALL→BOOT→MIND→ROOT→MESH automatically.",
|
||||
"modules": ["auth", "waf", "system", "billets", "podcasteur", "exposure", "wireguard"]
|
||||
}
|
||||
|
|
|
|||
11
secubox-companion/www/modules/system/module.json
Normal file
11
secubox-companion/www/modules/system/module.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"id": "system",
|
||||
"name": "System",
|
||||
"module": "BOOT",
|
||||
"icon": "⚙️",
|
||||
"description": "Services, health, resources.",
|
||||
"api_base": "/api/v1/system",
|
||||
"status_endpoint": "/api/v1/system/status",
|
||||
"capabilities": ["read"],
|
||||
"routes": [{ "path": "", "label": "Health" }]
|
||||
}
|
||||
34
secubox-companion/www/modules/system/view.js
Normal file
34
secubox-companion/www/modules/system/view.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
// SecuBox Companion :: system — health, resources, services (read).
|
||||
export default async function mount(ctx) {
|
||||
const { root, api, base, ui } = ctx;
|
||||
const { el, esc, clear } = ui;
|
||||
|
||||
const host = clear(root);
|
||||
host.append(el('p.muted', { text: 'Loading system health…' }));
|
||||
try {
|
||||
const s = await api.get(`${base}/status`);
|
||||
clear(host);
|
||||
const g = el('div.grid');
|
||||
const gauge = (label, val, unit = '') => g.append(el('div.card', { style: 'text-align:center' }, [
|
||||
el('div.data', { style: 'font-size:1.5rem;font-weight:700', text: (val ?? '—') + unit }),
|
||||
el('div.kicker', { text: label }),
|
||||
]));
|
||||
gauge('CPU', s.cpu ?? s.cpu_percent, '%');
|
||||
gauge('Memory', s.mem ?? s.memory_percent ?? s.mem_percent, '%');
|
||||
gauge('Disk', s.disk ?? s.disk_percent, '%');
|
||||
gauge('Uptime', s.uptime_days ?? s.uptime, s.uptime_days ? 'd' : '');
|
||||
host.append(g);
|
||||
|
||||
const svcs = await api.get(`${base}/services`).catch(() => null); // TODO(api): confirm services route
|
||||
if (svcs) {
|
||||
const items = svcs.services || svcs.items || [];
|
||||
const card = el('div.card', {}, [el('h3', { text: `Services (${items.length})` })]);
|
||||
for (const sv of items.slice(0, 60)) card.append(el('div.item', {}, [
|
||||
el('div.meta', {}, [el('b', { text: esc(sv.name || sv.unit) })]),
|
||||
el('span.badge' + (sv.active || sv.status === 'active' || sv.running ? '.ok' : '.err'), { text: sv.status || (sv.active ? 'active' : 'down') }),
|
||||
]));
|
||||
host.append(card);
|
||||
}
|
||||
} catch (e) { clear(host).append(el('div.empty', { text: 'Failed: ' + esc(e.message) })); }
|
||||
}
|
||||
11
secubox-companion/www/modules/waf/module.json
Normal file
11
secubox-companion/www/modules/waf/module.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"id": "waf",
|
||||
"name": "WAF",
|
||||
"module": "WALL",
|
||||
"icon": "🛡️",
|
||||
"description": "Web firewall: traffic, threats, MITM filters.",
|
||||
"api_base": "/api/v1/waf",
|
||||
"status_endpoint": "/api/v1/waf/status",
|
||||
"capabilities": ["read"],
|
||||
"routes": [{ "path": "", "label": "Overview" }]
|
||||
}
|
||||
34
secubox-companion/www/modules/waf/view.js
Normal file
34
secubox-companion/www/modules/waf/view.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
// SecuBox Companion :: waf — sbxwaf traffic/threats + MITM filter list (read).
|
||||
export default async function mount(ctx) {
|
||||
const { root, api, base, ui } = ctx;
|
||||
const { el, esc, clear } = ui;
|
||||
|
||||
const host = clear(root);
|
||||
host.append(el('p.muted', { text: 'Loading WAF stats…' }));
|
||||
try {
|
||||
const s = await api.get(`${base}/stats`).catch(() => api.get(`${base}/status`)); // TODO(api): confirm stats route
|
||||
clear(host);
|
||||
const stat = (label, val) => el('div.card', { style: 'text-align:center' }, [
|
||||
el('div.data', { style: 'font-size:1.6rem;font-weight:700', text: String(val ?? '—') }),
|
||||
el('div.kicker', { text: label }),
|
||||
]);
|
||||
host.append(el('div.grid', {}, [
|
||||
stat('Requests', s.requests ?? s.total ?? '—'),
|
||||
stat('Blocked', s.blocked ?? s.total_blocked ?? '—'),
|
||||
stat('Threats', s.threats ?? s.threats_count ?? '—'),
|
||||
stat('Backends', (s.backends || s.routes || []).length || s.backends_count || '—'),
|
||||
]));
|
||||
// MITM filters (bypass/splice exclusion set)
|
||||
const filt = await api.get(`${base}/filters`).catch(() => null); // TODO(api): confirm Filtres MITM route
|
||||
if (filt) {
|
||||
const items = filt.filters || filt.items || filt.exclusions || [];
|
||||
const card = el('div.card', {}, [el('h3', { text: `MITM filters (${items.length})` })]);
|
||||
for (const f of items.slice(0, 40)) card.append(el('div.item', {}, [
|
||||
el('div.meta', {}, [el('b', { text: f.host || f.domain || f }), el('span', { text: f.mode || f.kind || '' })]),
|
||||
el('span.badge', { text: f.enabled === false ? 'off' : 'on' }),
|
||||
]));
|
||||
host.append(card);
|
||||
}
|
||||
} catch (e) { clear(host).append(el('div.empty', { text: 'Failed: ' + esc(e.message) })); }
|
||||
}
|
||||
11
secubox-companion/www/modules/wireguard/module.json
Normal file
11
secubox-companion/www/modules/wireguard/module.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"id": "wireguard",
|
||||
"name": "WireGuard",
|
||||
"module": "MESH",
|
||||
"icon": "🔗",
|
||||
"description": "VPN tunnels: peers, handshakes, transfer.",
|
||||
"api_base": "/api/v1/wireguard",
|
||||
"status_endpoint": "/api/v1/wireguard/status",
|
||||
"capabilities": ["read"],
|
||||
"routes": [{ "path": "", "label": "Peers" }]
|
||||
}
|
||||
33
secubox-companion/www/modules/wireguard/view.js
Normal file
33
secubox-companion/www/modules/wireguard/view.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
// SecuBox Companion :: wireguard — tunnels & peers (read).
|
||||
export default async function mount(ctx) {
|
||||
const { root, api, base, ui } = ctx;
|
||||
const { el, esc, toast, ago, clear } = ui;
|
||||
|
||||
const host = clear(root);
|
||||
host.append(el('p.muted', { text: 'Loading tunnels…' }));
|
||||
try {
|
||||
const s = await api.get(`${base}/status`);
|
||||
const peers = await api.get(`${base}/peers`).catch(() => ({ peers: [] })); // TODO(api): confirm peers route
|
||||
clear(host);
|
||||
host.append(el('div.card', {}, [
|
||||
el('h3', { text: 'Status' }),
|
||||
el('div.mono-sm', { text: `interface: ${esc(s.interface || s.iface || '—')} · peers: ${(peers.peers || []).length}` }),
|
||||
]));
|
||||
const list = peers.peers || peers.items || [];
|
||||
if (!list.length) return host.append(el('div.empty', { text: 'No peers (or peers API not exposed — TODO(api)).' }));
|
||||
for (const p of list) {
|
||||
const hs = p.latest_handshake || p.handshake;
|
||||
const online = hs && (Date.now() - (typeof hs === 'number' ? hs * 1000 : Date.parse(hs))) < 180000;
|
||||
host.append(el('div.item', {}, [
|
||||
el('div.meta', {}, [
|
||||
el('b', { text: p.name || p.public_key?.slice(0, 16) || 'peer' }),
|
||||
el('span', { text: `${esc(p.endpoint || p.allowed_ips || '')} · ↑${fmt(p.tx_bytes || p.transfer_tx)} ↓${fmt(p.rx_bytes || p.transfer_rx)}` }),
|
||||
]),
|
||||
el('span.badge' + (online ? '.ok' : ''), { text: hs ? ago(typeof hs === 'number' ? hs * 1000 : hs) : 'never' }),
|
||||
]));
|
||||
}
|
||||
} catch (e) { clear(host).append(el('div.empty', { text: 'Failed: ' + esc(e.message) })); }
|
||||
|
||||
function fmt(b) { b = +b || 0; const u = ['B', 'K', 'M', 'G']; let i = 0; while (b >= 1024 && i < 3) { b /= 1024; i++; } return b.toFixed(i ? 1 : 0) + u[i]; }
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user