feat: build out the /plugins page
CI / TypeScript — type-check (push) Successful in 8s
CI / Publish — build & push image (push) Successful in 8s
CI / Deploy — pull & restart on host (push) Successful in 2s

The nav and footer have linked to /plugins since the start, but no
route or page existed, so it rendered blank. Adds the page per the
docs/PAGES.md spec — plugin building blocks, the four built-in
plugins, a step-by-step "writing a plugin" guide with real code from
netork/plugins/, and the fire/call/transform hook bus — sourced from
the actual plugin system in the main NetOrk repo for accuracy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Manivong
2026-06-30 13:37:56 +02:00
co-authored by Claude Sonnet 4.6
parent 2f8cbf48af
commit b37703bbbb
3 changed files with 277 additions and 0 deletions
+2
View File
@@ -7,6 +7,7 @@ import Drivers from './pages/Drivers'
import GettingStarted from './pages/GettingStarted' import GettingStarted from './pages/GettingStarted'
import Roadmap from './pages/Roadmap' import Roadmap from './pages/Roadmap'
import Nis2 from './pages/Nis2' import Nis2 from './pages/Nis2'
import Plugins from './pages/Plugins'
export default function App() { export default function App() {
return ( return (
@@ -21,6 +22,7 @@ export default function App() {
<Route path="/docs/getting-started" element={<GettingStarted />} /> <Route path="/docs/getting-started" element={<GettingStarted />} />
<Route path="/roadmap" element={<Roadmap />} /> <Route path="/roadmap" element={<Roadmap />} />
<Route path="/nis2" element={<Nis2 />} /> <Route path="/nis2" element={<Nis2 />} />
<Route path="/plugins" element={<Plugins />} />
</Routes> </Routes>
</main> </main>
<Footer /> <Footer />
+28
View File
@@ -254,6 +254,20 @@ const en = {
groupPlanned: 'Planned', groupPlanned: 'Planned',
groupConsidering: 'Under consideration', groupConsidering: 'Under consideration',
}, },
plugins: {
heading: 'Plugin System',
sub: 'Integrations are plugins, not core code. Wazuh, Graylog, CrowdSec, and apt-cacher-ng all register through the same pattern — metadata, hooks, tasks, router.',
whatHeading: 'What is a plugin?',
builtinHeading: 'Built-in plugins',
writingHeading: 'Writing a plugin',
hookBusHeading: 'Hook bus',
registryHeading: 'Plugin registry & enable/disable',
cta: {
heading: 'Add your own integration.',
body: 'One documented pattern — metadata, hooks, tasks, router. No core code changes required.',
button: 'Get started →',
},
},
nis2: { nis2: {
heading: 'NIS2 & netOrk', heading: 'NIS2 & netOrk',
sub: 'NIS2 Art. 21 defines ten categories of technical and organizational measures. Some of them are directly addressed by what netOrk does every day. This page maps each requirement to netOrk\'s current capabilities — honestly, including what\'s partial and what\'s not applicable.', sub: 'NIS2 Art. 21 defines ten categories of technical and organizational measures. Some of them are directly addressed by what netOrk does every day. This page maps each requirement to netOrk\'s current capabilities — honestly, including what\'s partial and what\'s not applicable.',
@@ -537,6 +551,20 @@ const de: Translations = {
groupPlanned: 'Geplant', groupPlanned: 'Geplant',
groupConsidering: 'In Erwägung', groupConsidering: 'In Erwägung',
}, },
plugins: {
heading: 'Plugin-System',
sub: 'Integrationen sind Plugins, kein Core-Code. Wazuh, Graylog, CrowdSec und apt-cacher-ng registrieren sich alle über dasselbe Muster — Metadaten, Hooks, Tasks, Router.',
whatHeading: 'Was ist ein Plugin?',
builtinHeading: 'Eingebaute Plugins',
writingHeading: 'Ein Plugin schreiben',
hookBusHeading: 'Hook-Bus',
registryHeading: 'Plugin-Registry & Aktivieren/Deaktivieren',
cta: {
heading: 'Eigene Integration hinzufügen.',
body: 'Ein dokumentiertes Muster — Metadaten, Hooks, Tasks, Router. Keine Änderungen am Core-Code nötig.',
button: 'Loslegen →',
},
},
nis2: { nis2: {
heading: 'NIS2 & netOrk', heading: 'NIS2 & netOrk',
sub: 'NIS2 Art. 21 definiert zehn Kategorien technischer und organisatorischer Maßnahmen. Einige werden durch das, was netOrk täglich tut, direkt adressiert. Diese Seite ordnet jede Anforderung den aktuellen netOrk-Funktionen zu — ehrlich, einschließlich was teilweise abgedeckt ist und was nicht anwendbar ist.', sub: 'NIS2 Art. 21 definiert zehn Kategorien technischer und organisatorischer Maßnahmen. Einige werden durch das, was netOrk täglich tut, direkt adressiert. Diese Seite ordnet jede Anforderung den aktuellen netOrk-Funktionen zu — ehrlich, einschließlich was teilweise abgedeckt ist und was nicht anwendbar ist.',
+247
View File
@@ -0,0 +1,247 @@
import { Link } from 'react-router-dom'
import { PuzzlePieceIcon } from '@heroicons/react/24/outline'
import { useLang } from '../context/LangContext'
type Building = { title: string; body: string }
type BuiltinPlugin = { label: string; body: string; hosts: string[]; hooks: string[] }
type Step = { title: string; body: string }
type HookKind = { name: string; behavior: string; use: string }
const BUILDING_BLOCKS: Record<'en' | 'de', Building[]> = {
en: [
{ title: 'Metadata (PluginMeta)', body: 'name, label, description, version, a declared permission surface (db_read, db_write, external_hosts, hooks — for review, not runtime enforcement), and an optional Celery Beat schedule.' },
{ title: 'Router', body: 'A FastAPI APIRouter, mounted under /api/v1 at startup — but only for plugins that are enabled.' },
{ title: 'Tasks', body: 'Celery tasks for anything that talks to an external system. Hooks queue work; they never block the event loop on I/O.' },
{ title: 'Hooks', body: 'Handlers registered against the central event bus (hook_registry) via the @hook decorator — react to events like poll.complete.' },
],
de: [
{ title: 'Metadaten (PluginMeta)', body: 'name, label, description, version, eine deklarierte Permission-Surface (db_read, db_write, external_hosts, hooks — für Review, nicht zur Laufzeit erzwungen) und ein optionaler Celery-Beat-Schedule.' },
{ title: 'Router', body: 'Ein FastAPI APIRouter, beim Start unter /api/v1 gemountet — aber nur für aktivierte Plugins.' },
{ title: 'Tasks', body: 'Celery-Tasks für alles, was mit einem externen System spricht. Hooks reihen Arbeit ein; sie blockieren den Event-Loop nie mit I/O.' },
{ title: 'Hooks', body: 'Handler, die über den @hook-Decorator am zentralen Event-Bus (hook_registry) registriert werden — reagieren auf Events wie poll.complete.' },
],
}
const BUILTIN: Record<'en' | 'de', BuiltinPlugin[]> = {
en: [
{ label: 'Wazuh Security', body: 'Syncs device enrollment with the Wazuh manager and surfaces agent health warnings, vulnerability counts, alerts, and CIS benchmark scores.', hosts: ['wazuh-manager:55000', 'wazuh-indexer:9200'], hooks: ['poll.complete'] },
{ label: 'Graylog Syslog', body: 'Checks whether syslog-capable devices forward logs to Graylog via rsyslog, and can automatically write the forwarding rule.', hosts: [], hooks: [] },
{ label: 'CrowdSec', body: 'Surfaces org-level CrowdSec security health data per device: active decisions, blocked requests, remediation metrics, and top attack scenarios.', hosts: ['admin.api.crowdsec.net:443'], hooks: [] },
{ label: 'apt-cacher-ng', body: "Auto-detects apt-cacher-ng on Docker hosts and configures the APT proxy on devices within the same site so APT traffic is routed through the local cache.", hosts: [], hooks: ['poll.complete'] },
],
de: [
{ label: 'Wazuh Security', body: 'Synchronisiert die Geräte-Registrierung mit dem Wazuh-Manager und zeigt Agent-Health-Warnungen, Schwachstellenanzahl, Alerts und CIS-Benchmark-Scores an.', hosts: ['wazuh-manager:55000', 'wazuh-indexer:9200'], hooks: ['poll.complete'] },
{ label: 'Graylog Syslog', body: 'Prüft, ob syslog-fähige Geräte Logs via rsyslog an Graylog weiterleiten, und kann die Weiterleitungsregel automatisch schreiben.', hosts: [], hooks: [] },
{ label: 'CrowdSec', body: 'Zeigt Org-Level-CrowdSec-Sicherheitsdaten pro Gerät: aktive Entscheidungen, geblockte Requests, Remediation-Metriken und Top-Angriffsszenarien.', hosts: ['admin.api.crowdsec.net:443'], hooks: [] },
{ label: 'apt-cacher-ng', body: 'Erkennt apt-cacher-ng automatisch auf Docker-Hosts und konfiguriert den APT-Proxy auf Geräten im selben Standort, sodass APT-Traffic über den lokalen Cache läuft.', hosts: [], hooks: ['poll.complete'] },
],
}
const STEPS: Record<'en' | 'de', Step[]> = {
en: [
{ title: 'Declare metadata', body: 'Create netork/plugins/<name>/__init__.py with a PluginMeta — permissions are declarative, for human review, not enforced at runtime.' },
{ title: 'Register', body: 'Implement register(): register the metadata with plugin_registry and import your hooks module so its @hook decorators run.' },
{ title: 'Add hooks', body: 'hooks.py: handlers for the events you care about, e.g. poll.complete.' },
{ title: 'Add a router (optional)', body: 'router.py: a FastAPI APIRouter, returned from a _get_router() callable passed to plugin_registry.register().' },
{ title: 'Add tasks', body: 'tasks.py: Celery tasks for anything that talks to an external system — hooks should queue work, not block.' },
],
de: [
{ title: 'Metadaten deklarieren', body: 'netork/plugins/<name>/__init__.py mit einer PluginMeta anlegen — Permissions sind deklarativ, für menschliches Review, nicht zur Laufzeit erzwungen.' },
{ title: 'Registrieren', body: 'register() implementieren: Metadaten bei plugin_registry registrieren und das hooks-Modul importieren, damit dessen @hook-Decorators laufen.' },
{ title: 'Hooks hinzufügen', body: 'hooks.py: Handler für die Events, die dich interessieren, z. B. poll.complete.' },
{ title: 'Router hinzufügen (optional)', body: 'router.py: ein FastAPI APIRouter, zurückgegeben von einem _get_router()-Callable, das an plugin_registry.register() übergeben wird.' },
{ title: 'Tasks hinzufügen', body: 'tasks.py: Celery-Tasks für alles, was mit einem externen System spricht — Hooks sollten Arbeit einreihen, nicht blockieren.' },
],
}
const HOOK_KINDS: Record<'en' | 'de', HookKind[]> = {
en: [
{ name: 'fire()', behavior: 'Fire-and-forget. All handlers run concurrently; exceptions are logged, never raised. 30 s timeout per handler.', use: 'Side effects after an event, e.g. "queue a Wazuh sync after a poll completes".' },
{ name: 'call()', behavior: 'Blocking. Handlers run sequentially by priority; exceptions propagate to the caller. 10 s timeout per handler.', use: 'When the caller needs to know a handler failed.' },
{ name: 'transform()', behavior: 'Pipeline. Handlers run sequentially, each receiving and returning value. 10 s timeout per handler.', use: 'Letting plugins enrich or modify a value in place, e.g. annotate a payload before it\'s persisted.' },
],
de: [
{ name: 'fire()', behavior: 'Fire-and-forget. Alle Handler laufen gleichzeitig; Exceptions werden geloggt, nie geworfen. 30 s Timeout pro Handler.', use: 'Seiteneffekte nach einem Event, z. B. „Wazuh-Sync nach abgeschlossenem Poll einreihen".' },
{ name: 'call()', behavior: 'Blockierend. Handler laufen sequenziell nach Priorität; Exceptions propagieren zum Aufrufer. 10 s Timeout pro Handler.', use: 'Wenn der Aufrufer wissen muss, dass ein Handler fehlgeschlagen ist.' },
{ name: 'transform()', behavior: 'Pipeline. Handler laufen sequenziell, jeder erhält und liefert value zurück. 10 s Timeout pro Handler.', use: 'Plugins lassen einen Wert anreichern oder verändern, z. B. ein Payload vor dem Speichern annotieren.' },
],
}
const REGISTRY_PARAGRAPHS: Record<'en' | 'de', string[]> = {
en: [
'Enabled state lives in the database as a Setting row: plugin.<name>.enabled = "true" | "false". The registry exposes enable() and disable(), gated behind RBAC like every other orchestration action.',
"Routers are mounted once at FastAPI startup for every enabled plugin — FastAPI doesn't support unmounting routers at runtime, so disabling a plugin's API surface takes effect after the next deploy.",
"Hook handlers are registered as soon as a plugin's hooks module is imported. Whether a handler is a no-op until configured is up to the handler itself — the bundled plugins check their own Settings before acting.",
],
de: [
'Der Aktivierungsstatus liegt in der Datenbank als Setting-Zeile: plugin.<name>.enabled = "true" | "false". Die Registry stellt enable() und disable() bereit, abgesichert durch RBAC wie jede andere Orchestrierungsaktion.',
'Router werden beim FastAPI-Start einmal für jedes aktivierte Plugin gemountet — FastAPI unterstützt kein Unmounten zur Laufzeit, daher wirkt sich das Deaktivieren der API-Oberfläche eines Plugins erst nach dem nächsten Deploy aus.',
'Hook-Handler werden registriert, sobald das hooks-Modul eines Plugins importiert wird. Ob ein Handler bis zur Konfiguration ein No-op bleibt, entscheidet der Handler selbst — die mitgelieferten Plugins prüfen vor dem Handeln ihre eigenen Settings.',
],
}
const META_CODE = `# netork/plugins/myplugin/__init__.py
from netork.plugins import PluginMeta, PluginPermissions
from netork.plugins.registry import plugin_registry
_META = PluginMeta(
name="myplugin",
label="My Plugin",
description="What this plugin does.",
permissions=PluginPermissions(
db_read=["Device"],
external_hosts=["myservice:443"],
hooks=["poll.complete"],
),
)
def register() -> None:
plugin_registry.register(_META, get_router=_get_router)
from netork.plugins.myplugin import hooks as _hooks # noqa: F401
def _get_router():
from netork.plugins.myplugin.router import router
return router`
const HOOK_CODE = `# netork/plugins/myplugin/hooks.py
from netork.plugins.hooks import hook
@hook("poll.complete", priority=100)
async def _on_poll_complete(device_id: str, session_factory, **kwargs) -> None:
# queue a Celery task — never block the event loop here
from netork.plugins.myplugin.tasks import sync_device
sync_device.apply_async(args=[device_id], queue="default")`
function CodeBlock({ code }: { code: string }) {
return (
<pre className="rounded-xl border border-slate-800 bg-slate-900 p-6 font-mono text-sm text-slate-300 overflow-x-auto">
<code>{code}</code>
</pre>
)
}
function HostTag({ value }: { value: string }) {
return (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-mono text-slate-400 border border-slate-700 bg-slate-950">
{value}
</span>
)
}
export default function Plugins() {
const { lang, t } = useLang()
const blocks = BUILDING_BLOCKS[lang]
const builtin = BUILTIN[lang]
const steps = STEPS[lang]
const hookKinds = HOOK_KINDS[lang]
const registryParagraphs = REGISTRY_PARAGRAPHS[lang]
return (
<div className="py-16 md:py-24">
<div className="max-w-4xl mx-auto px-6">
<div className="mb-16">
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-sky-600/10">
<PuzzlePieceIcon className="h-5 w-5 text-sky-400" />
</div>
<h1 className="text-4xl md:text-5xl font-bold text-slate-100 mb-4">{t.plugins.heading}</h1>
<p className="text-base text-slate-400 leading-relaxed max-w-2xl">{t.plugins.sub}</p>
</div>
<div className="mb-20">
<h2 className="text-xl font-semibold text-slate-200 mb-6 pb-2 border-b border-slate-800">
{t.plugins.whatHeading}
</h2>
<div className="grid sm:grid-cols-2 gap-6">
{blocks.map((b) => (
<div key={b.title} className="rounded-xl border border-slate-800 bg-slate-900 p-5">
<p className="text-sm font-semibold text-slate-200 mb-1.5">{b.title}</p>
<p className="text-sm text-slate-500 leading-relaxed">{b.body}</p>
</div>
))}
</div>
</div>
<div className="mb-20">
<h2 className="text-xl font-semibold text-slate-200 mb-6 pb-2 border-b border-slate-800">
{t.plugins.builtinHeading}
</h2>
<div className="grid sm:grid-cols-2 gap-6">
{builtin.map((p) => (
<div key={p.label} className="rounded-xl border border-slate-800 bg-slate-900 p-5">
<p className="text-sm font-semibold text-slate-200 mb-1.5">{p.label}</p>
<p className="text-sm text-slate-500 leading-relaxed mb-3">{p.body}</p>
{(p.hosts.length > 0 || p.hooks.length > 0) && (
<div className="flex flex-wrap gap-1.5">
{p.hosts.map((h) => <HostTag key={h} value={h} />)}
{p.hooks.map((h) => <HostTag key={h} value={`hook: ${h}`} />)}
</div>
)}
</div>
))}
</div>
</div>
<div className="mb-20">
<h2 className="text-xl font-semibold text-slate-200 mb-6 pb-2 border-b border-slate-800">
{t.plugins.writingHeading}
</h2>
<ol className="space-y-4 mb-8">
{steps.map((s, i) => (
<li key={s.title} className="flex gap-4">
<span className="shrink-0 h-6 w-6 rounded-full bg-sky-600/10 text-sky-400 text-xs font-semibold flex items-center justify-center mt-0.5">
{i + 1}
</span>
<div>
<p className="text-sm font-medium text-slate-200">{s.title}</p>
<p className="text-sm text-slate-500 leading-relaxed">{s.body}</p>
</div>
</li>
))}
</ol>
<div className="space-y-4">
<CodeBlock code={META_CODE} />
<CodeBlock code={HOOK_CODE} />
</div>
</div>
<div className="mb-20">
<h2 className="text-xl font-semibold text-slate-200 mb-6 pb-2 border-b border-slate-800">
{t.plugins.hookBusHeading}
</h2>
<div className="space-y-4">
{hookKinds.map((h) => (
<div key={h.name} className="rounded-xl border border-slate-800 bg-slate-900 p-5">
<p className="text-sm font-mono font-semibold text-sky-400 mb-1.5">{h.name}</p>
<p className="text-sm text-slate-400 leading-relaxed mb-2">{h.behavior}</p>
<p className="text-sm text-slate-500 leading-relaxed">{h.use}</p>
</div>
))}
</div>
</div>
<div className="mb-20">
<h2 className="text-xl font-semibold text-slate-200 mb-6 pb-2 border-b border-slate-800">
{t.plugins.registryHeading}
</h2>
<div className="space-y-4">
{registryParagraphs.map((p) => (
<p key={p} className="text-sm text-slate-400 leading-relaxed">{p}</p>
))}
</div>
</div>
<div className="rounded-xl border border-slate-800 bg-slate-900 p-8 text-center">
<h2 className="text-xl font-semibold text-slate-100 mb-3">{t.plugins.cta.heading}</h2>
<p className="text-sm text-slate-400 leading-relaxed mb-6 max-w-md mx-auto">{t.plugins.cta.body}</p>
<Link
to="/docs/getting-started"
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-sky-600 hover:bg-sky-500 text-white text-sm font-medium transition-colors"
>
{t.plugins.cta.button}
</Link>
</div>
</div>
</div>
)
}