feat: redesign — one message, one audience, light pages
The site felt old, unfocused and bloated: 11 pages, a features page of 171 bullets, a homepage of 830 words in card grids around seven mockup-style screenshots, and no single thing it wanted a visitor to understand. - Message: control instead of drift. Audience: IT departments in small and mid-sized companies. Goal: buy a licence — netOrk itself is free, the licence adds vulnerability data and image updates. - Home is under 400 words: the drift comparison of a real access point, three steps, the hardware it runs on, vulnerabilities with a licence, what else is in the box, one closing band. Features, Drivers and Roadmap are gone; their URLs redirect (router and nginx 301). - New Pricing page: the free core, Starter / Pro / Enterprise on request with the plan differences from the licence server, four questions; buttons go to the licence portal. The unit-less KB request limit is left out. - Persona pages are one template; NIS2 and Plugins are cut to half or less. Impressum and Datenschutz exist as marked placeholders; the unsupported "MIT licence" claim is gone from the footer. - Look: light paper and ink, the dark product on a stage, Inter self-hosted, split sections and ruled lists instead of cards. Four real, cropped screenshots replace eight full-window ones. - Language follows the browser until someone chooses; <html lang> is set. Scroll-to-top on navigation, a catch-all route, no dead /docs/architecture. - CLAUDE.md, DESIGN.md, PAGES.md and PRODUCT.md describe the new rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
369f66afdc
commit
f70fec496a
+44
-220
@@ -1,233 +1,57 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ShieldCheckIcon } from '@heroicons/react/24/outline'
|
||||
import { useLang } from '../context/LangContext'
|
||||
import { usePageTitle } from '../lib/usePageTitle'
|
||||
import { linkify } from '../lib/glossary'
|
||||
|
||||
type Coverage = 'covered' | 'partial' | 'roadmap' | 'na'
|
||||
type Requirement = { article: string; label: string; coverage: Coverage; netork: string }
|
||||
type EvidenceBlock = { trigger: string; produces: string[] }
|
||||
type ComingItem = { title: string; detail: string }
|
||||
|
||||
const REQUIREMENTS: Record<'en' | 'de', Requirement[]> = {
|
||||
en: [
|
||||
{ article: 'Art. 21 (2a)', label: 'Risk analysis & information system security policies', coverage: 'partial', netork: 'Config drift detection, SNMP health metrics, and security agent coverage across all devices provide a continuous risk baseline. A formal risk register is out of scope for netOrk.' },
|
||||
{ article: 'Art. 21 (2b)', label: 'Incident handling', coverage: 'partial', netork: 'Wazuh alert history, CrowdSec decisions, and Graylog syslog per device surface incidents at the network layer. A structured incident record with NIS2 Art. 23 reporting timers is on the roadmap.' },
|
||||
{ article: 'Art. 21 (2c)', label: 'Business continuity, backup management, disaster recovery', coverage: 'partial', netork: 'Every poll captures a configuration snapshot into a local Git repository — full history, a side-by-side diff viewer between any two points in time, and one-click restore for OPNsense. Backup/recovery for full device state beyond configuration is out of scope.' },
|
||||
{ article: 'Art. 21 (2d)', label: 'Supply chain security', coverage: 'covered', netork: 'Vendor, model, firmware, and OS version are tracked per device after every poll. The EOL Tracking plugin checks each device\'s OS version against the endoflife.date API daily and flags unsupported or soon-to-be-unsupported software.' },
|
||||
{ article: 'Art. 21 (2e)', label: 'Vulnerability handling in acquisition, development & maintenance', coverage: 'covered', netork: 'Installed software and container images on every device are matched against known vulnerabilities, rated by what each flaw means on that device. A triage queue records a decision per vulnerability — not applicable, accepted until, deferred until, fixed — each with a reason, a date and who decided, written to the audit log. Deferrals come back by themselves, and a daily reassessment verifies fixes. Vulnerability data requires a netOrk licence.' },
|
||||
{ article: 'Art. 21 (2f)', label: 'Assessing effectiveness of cybersecurity measures', coverage: 'partial', netork: 'The audit log records all orchestration actions. A per-site compliance dashboard (on roadmap) will aggregate security agent coverage, drift status, and patch metrics into a single view.' },
|
||||
{ article: 'Art. 21 (2g)', label: 'Basic cyber hygiene & cybersecurity training', coverage: 'na', netork: 'Out of scope for a network orchestration platform. Training and hygiene policies are handled at the organizational level.' },
|
||||
{ article: 'Art. 21 (2h)', label: 'Access control, asset management, human resources security', coverage: 'covered', netork: 'Full device inventory maintained automatically via discovery and continuous polling. RBAC with four built-in roles (viewer / operator / engineer / administrator) and custom role combinations. Complete audit log of all orchestration actions, filterable by date range, user, action, or resource — export to CSV or PDF for audit submissions.' },
|
||||
{ article: 'Art. 21 (2i)', label: 'Multi-factor authentication', coverage: 'covered', netork: 'TOTP-based MFA for netOrk user accounts — authenticator app at login, backup codes for emergencies, session invalidation on TOTP changes, enforceable per role. Terminal sessions to devices log in with each user\'s own SSH key, never a shared account, and opened and refused sessions are recorded.' },
|
||||
{ article: 'Art. 21 (2j)', label: 'Physical and environmental security', coverage: 'na', netork: 'Out of scope. Physical security of the infrastructure hosting netOrk is an organizational and facility concern.' },
|
||||
],
|
||||
de: [
|
||||
{ article: 'Art. 21 (2a)', label: 'Risikoanalyse und Sicherheitsrichtlinien für Informationssysteme', coverage: 'partial', netork: 'Konfigurationsdrift-Erkennung, SNMP-Gesundheitsmetriken und Security-Agent-Abdeckung über alle Geräte liefern eine kontinuierliche Risiko-Baseline. Ein formales Risikoregister liegt außerhalb des Scopes von netOrk.' },
|
||||
{ article: 'Art. 21 (2b)', label: 'Bewältigung von Sicherheitsvorfällen', coverage: 'partial', netork: 'Wazuh-Alert-Historie, CrowdSec-Entscheidungen und Graylog-Syslog pro Gerät decken Vorfälle auf Netzwerkebene auf. Ein strukturierter Incident-Datensatz mit NIS2 Art. 23 Melde-Timern ist auf der Roadmap.' },
|
||||
{ article: 'Art. 21 (2c)', label: 'Geschäftskontinuität, Backup-Management, Disaster Recovery', coverage: 'partial', netork: 'Bei jedem Poll wird ein Konfigurationssnapshot in einem lokalen Git-Repository gespeichert — vollständige Historie, ein Side-by-Side-Diff-Viewer zwischen beliebigen Zeitpunkten und Ein-Klick-Restore für OPNsense. Backup/Recovery für den vollständigen Gerätezustand über die Konfiguration hinaus liegt außerhalb des Scopes.' },
|
||||
{ article: 'Art. 21 (2d)', label: 'Supply-Chain-Sicherheit', coverage: 'covered', netork: 'Hersteller, Modell, Firmware und OS-Version werden nach jedem Poll pro Gerät erfasst. Das EOL-Tracking-Plugin gleicht die OS-Version jedes Geräts täglich mit der endoflife.date-API ab und kennzeichnet nicht mehr oder bald nicht mehr unterstützte Software.' },
|
||||
{ article: 'Art. 21 (2e)', label: 'Schwachstellenbehandlung bei Erwerb, Entwicklung & Wartung', coverage: 'covered', netork: 'Installierte Software und Container-Images jedes Geräts werden gegen bekannte Schwachstellen abgeglichen und danach bewertet, was die Lücke auf genau diesem Gerät bedeutet. Eine Triage-Queue hält pro Schwachstelle eine Entscheidung fest — nicht zutreffend, akzeptiert bis, zurückgestellt bis, behoben — jeweils mit Begründung, Datum und Entscheider, im Audit-Log protokolliert. Zurückgestelltes kommt von selbst zurück, eine tägliche Neubewertung verifiziert Behebungen. Die Schwachstellendaten setzen eine netOrk-Lizenz voraus.' },
|
||||
{ article: 'Art. 21 (2f)', label: 'Beurteilung der Wirksamkeit von Cybersicherheitsmaßnahmen', coverage: 'partial', netork: 'Das Audit-Log erfasst alle Orchestrierungsaktionen. Ein Compliance-Dashboard pro Standort (auf der Roadmap) wird Security-Agent-Abdeckung, Drift-Status und Patch-Metriken in einer Ansicht zusammenfassen.' },
|
||||
{ article: 'Art. 21 (2g)', label: 'Grundlegende Cyberhygiene und Cybersicherheitsschulungen', coverage: 'na', netork: 'Außerhalb des Scopes einer Netzwerk-Orchestrierungsplattform. Schulungen und Hygiene-Richtlinien werden auf Organisationsebene gehandhabt.' },
|
||||
{ article: 'Art. 21 (2h)', label: 'Zugangskontrolle, Asset-Management, Personalsicherheit', coverage: 'covered', netork: 'Vollständiges Geräteinventar automatisch über Discovery und kontinuierliches Polling gepflegt. RBAC mit vier integrierten Rollen (Betrachter / Operator / Ingenieur / Administrator) und benutzerdefinierten Rollenkombinationen. Vollständiges Audit-Log aller Orchestrierungsaktionen, filterbar nach Datumsbereich, Benutzer, Aktion oder Ressource — Export als CSV oder PDF für Audit-Einreichungen.' },
|
||||
{ article: 'Art. 21 (2i)', label: 'Multi-Faktor-Authentifizierung', coverage: 'covered', netork: 'TOTP-basierte MFA für netOrk-Benutzerkonten — Authenticator-App beim Login, Backup-Codes für Notfälle, Session-Invalidierung bei TOTP-Änderungen, pro Rolle erzwingbar. Terminal-Sitzungen zu Geräten melden sich mit dem eigenen SSH-Schlüssel des Benutzers an, nie mit einem geteilten Konto; geöffnete und verweigerte Sitzungen werden protokolliert.' },
|
||||
{ article: 'Art. 21 (2j)', label: 'Physische und umgebungsbezogene Sicherheit', coverage: 'na', netork: 'Außerhalb des Scopes. Die physische Sicherheit der Infrastruktur, die netOrk hostet, ist eine organisatorische und gebäudetechnische Angelegenheit.' },
|
||||
],
|
||||
}
|
||||
|
||||
const EVIDENCE: Record<'en' | 'de', EvidenceBlock[]> = {
|
||||
en: [
|
||||
{
|
||||
trigger: 'Every device poll',
|
||||
produces: [
|
||||
'Installed package list with versions',
|
||||
'Available update count',
|
||||
'Interface status, ARP table, DHCP leases',
|
||||
'VLAN membership vs. desired state (drift)',
|
||||
'SNMP health metrics (CPU, memory, interface counters)',
|
||||
'Wazuh agent status and CVE counts by severity',
|
||||
'Graylog syslog forwarding status',
|
||||
'CrowdSec decisions and ban counts',
|
||||
'Git-backed configuration snapshot, diffed against the previous one to detect unauthorized changes',
|
||||
],
|
||||
},
|
||||
{
|
||||
trigger: 'Every user action',
|
||||
produces: [
|
||||
'Audit log entry: user, timestamp, resource, action',
|
||||
'Before/after values for configuration changes',
|
||||
'Acknowledged warnings logged with the accepting user',
|
||||
],
|
||||
},
|
||||
{
|
||||
trigger: 'On demand',
|
||||
produces: [
|
||||
'Topology graph — network segmentation view',
|
||||
'Subnet browser — IP space coverage',
|
||||
'VLAN matrix — which devices carry which VLANs',
|
||||
'Configuration diff between any two snapshots; one-click restore (OPNsense)',
|
||||
'Audit log export to PDF / CSV, filtered by date range, user, action, or resource',
|
||||
],
|
||||
},
|
||||
],
|
||||
de: [
|
||||
{
|
||||
trigger: 'Bei jedem Geräte-Poll',
|
||||
produces: [
|
||||
'Installierte Paketliste mit Versionen',
|
||||
'Anzahl verfügbarer Updates',
|
||||
'Schnittstellenstatus, ARP-Tabelle, DHCP-Leases',
|
||||
'VLAN-Zugehörigkeit vs. Sollzustand (Drift)',
|
||||
'SNMP-Gesundheitsmetriken (CPU, Speicher, Schnittstellenzähler)',
|
||||
'Wazuh-Agent-Status und CVE-Anzahl nach Schweregrad',
|
||||
'Graylog-Syslog-Weiterleitungsstatus',
|
||||
'CrowdSec-Entscheidungen und Ban-Anzahl',
|
||||
'Git-basierter Konfigurationssnapshot, gegen den vorherigen geprüft, um nicht autorisierte Änderungen zu erkennen',
|
||||
],
|
||||
},
|
||||
{
|
||||
trigger: 'Bei jeder Benutzeraktion',
|
||||
produces: [
|
||||
'Audit-Log-Eintrag: Benutzer, Zeitstempel, Ressource, Aktion',
|
||||
'Vorher/Nachher-Werte für Konfigurationsänderungen',
|
||||
'Bestätigte (acked) Warnungen werden mit dem bestätigenden Benutzer protokolliert',
|
||||
],
|
||||
},
|
||||
{
|
||||
trigger: 'Auf Abruf',
|
||||
produces: [
|
||||
'Topologie-Graph — Netzwerksegmentierungs-Ansicht',
|
||||
'Subnetz-Browser — IP-Raum-Abdeckung',
|
||||
'VLAN-Matrix — welche Geräte welche VLANs führen',
|
||||
'Konfigurations-Diff zwischen zwei beliebigen Snapshots; Ein-Klick-Restore (OPNsense)',
|
||||
'Audit-Log-Export als PDF / CSV, gefiltert nach Datumsbereich, Benutzer, Aktion oder Ressource',
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const COMING: Record<'en' | 'de', ComingItem[]> = {
|
||||
en: [
|
||||
{ title: 'Compliance dashboard', detail: 'Per-site Art. 21 checklist: asset coverage, patch status, agent deployment, drift, syslog, audit activity.' },
|
||||
{ title: 'Incident workflow', detail: 'Structured incident records with NIS2 Art. 23 reporting timers (24 h / 72 h) and external webhook delivery.' },
|
||||
],
|
||||
de: [
|
||||
{ title: 'Compliance-Dashboard', detail: 'Art. 21-Checkliste pro Standort: Asset-Abdeckung, Patch-Status, Agent-Deployment, Drift, Syslog, Audit-Aktivität.' },
|
||||
{ title: 'Incident-Workflow', detail: 'Strukturierte Incident-Datensätze mit NIS2 Art. 23 Melde-Timern (24 h / 72 h) und externer Webhook-Zustellung.' },
|
||||
],
|
||||
}
|
||||
|
||||
function CoverageTag({ coverage, labels }: { coverage: Coverage; labels: Record<string, string> }) {
|
||||
const base = 'inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium whitespace-nowrap border'
|
||||
if (coverage === 'covered') return <span className={`${base} bg-green-500/15 text-green-400 border-green-500/20`}>{labels.covered}</span>
|
||||
if (coverage === 'partial') return <span className={`${base} bg-yellow-500/15 text-yellow-400 border-yellow-500/20`}>{labels.partial}</span>
|
||||
if (coverage === 'roadmap') return <span className={`${base} bg-sky-500/15 text-sky-400 border-sky-500/20`}>{labels.roadmap}</span>
|
||||
return <span className={`${base} bg-slate-700/50 text-slate-500 border-slate-700`}>{labels.na}</span>
|
||||
}
|
||||
import Shot from '../components/Shot'
|
||||
import { ButtonLink, CtaBand, H2, PageHeader, Section, Split, Status } from '../components/ui'
|
||||
|
||||
export default function Nis2() {
|
||||
const { lang, t } = useLang()
|
||||
const reqs = REQUIREMENTS[lang]
|
||||
const evidence = EVIDENCE[lang]
|
||||
const coming = COMING[lang]
|
||||
const coverageLabels = t.nis2.coverageLabels
|
||||
const legendLabels = t.nis2.legendLabels
|
||||
const { t } = useLang()
|
||||
const n = t.nis2
|
||||
usePageTitle(t.meta.nis2)
|
||||
|
||||
return (
|
||||
<div className="py-16 md:py-24">
|
||||
<div className="max-w-4xl mx-auto px-6">
|
||||
<>
|
||||
<PageHeader eyebrow={n.eyebrow} title={n.title} lead={linkify(n.lead)} />
|
||||
|
||||
<div className="mb-16">
|
||||
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-sky-600/10">
|
||||
<ShieldCheckIcon className="h-5 w-5 text-sky-400" />
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-slate-100 mb-4">{t.nis2.heading}</h1>
|
||||
<p className="text-base text-slate-400 leading-relaxed max-w-2xl">{linkify(t.nis2.sub)}</p>
|
||||
</div>
|
||||
<Section className="pt-0 md:pt-0">
|
||||
<H2>{n.tableTitle}</H2>
|
||||
<dl className="mt-10">
|
||||
{n.requirements.map((r) => (
|
||||
<div
|
||||
key={r.article}
|
||||
className="grid gap-x-8 gap-y-2 border-t border-line py-5 md:grid-cols-[6rem_16rem_1fr_8rem]"
|
||||
>
|
||||
<dt className="font-mono text-sm text-accent">Art. 21 {r.article}</dt>
|
||||
<dt className="text-h3 text-ink">{r.label}</dt>
|
||||
<dd className="text-ink-soft">{linkify(r.body)}</dd>
|
||||
<dd className="md:text-right">
|
||||
<Status coverage={r.coverage} label={n.coverage[r.coverage]} />
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</Section>
|
||||
|
||||
<div className="mb-20">
|
||||
<h2 className="text-xl font-semibold text-slate-200 mb-6 pb-2 border-b border-slate-800">
|
||||
{t.nis2.mappingHeading}
|
||||
</h2>
|
||||
<div className="space-y-0 rounded-xl border border-slate-800 overflow-hidden">
|
||||
{reqs.map((r, i) => (
|
||||
<div key={r.article} className={`p-5 ${i < reqs.length - 1 ? 'border-b border-slate-800' : ''}`}>
|
||||
<div className="flex flex-wrap items-start justify-between gap-3 mb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs font-mono text-sky-500 shrink-0">{r.article}</span>
|
||||
<span className="text-sm font-medium text-slate-200">{r.label}</span>
|
||||
</div>
|
||||
<CoverageTag coverage={r.coverage} labels={coverageLabels} />
|
||||
</div>
|
||||
<p className="text-sm text-slate-500 leading-relaxed md:pl-24">{linkify(r.netork)}</p>
|
||||
</div>
|
||||
<Section band>
|
||||
<Split aside={<H2>{n.evidenceTitle}</H2>}>
|
||||
<ul>
|
||||
{n.evidence.map((e) => (
|
||||
<li key={e} className="border-t border-line py-4 text-ink-soft first:border-t-0 first:pt-0">
|
||||
{linkify(e)}
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap gap-4 text-xs text-slate-500">
|
||||
{(['covered', 'partial', 'roadmap', 'na'] as Coverage[]).map((c) => (
|
||||
<span key={c} className="flex items-center gap-1.5">
|
||||
<CoverageTag coverage={c} labels={coverageLabels} />
|
||||
{legendLabels[c]}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</ul>
|
||||
</Split>
|
||||
<div className="mt-14">
|
||||
<Shot name="audit-log" alt={t.shots.auditLog} caption={n.auditCaption} />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="mb-20">
|
||||
<h2 className="text-xl font-semibold text-slate-200 mb-2 pb-2 border-b border-slate-800">
|
||||
{t.nis2.evidenceHeading}
|
||||
</h2>
|
||||
<p className="text-sm text-slate-500 mb-6">{linkify(t.nis2.evidenceSub)}</p>
|
||||
<div className="space-y-6">
|
||||
{evidence.map((e) => (
|
||||
<div key={e.trigger} className="rounded-xl border border-slate-800 bg-slate-900 p-5">
|
||||
<p className="text-xs font-semibold text-sky-500 uppercase tracking-widest mb-3">{e.trigger}</p>
|
||||
<ul className="space-y-1.5">
|
||||
{e.produces.map((item) => (
|
||||
<li key={item} className="flex items-start gap-2 text-sm text-slate-400">
|
||||
<span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-sky-500" />
|
||||
{linkify(item)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</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.nis2.comingHeading}
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
{coming.map((item) => (
|
||||
<div key={item.title} className="flex gap-4">
|
||||
<div className="mt-2 shrink-0 h-1.5 w-1.5 rounded-full bg-sky-500" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-200">{item.title}</p>
|
||||
<p className="text-sm text-slate-500 mt-0.5">{linkify(item.detail)}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<Link to="/roadmap" className="text-sky-400 hover:text-sky-300 transition-colors text-sm font-medium">
|
||||
{lang === 'de' ? 'Vollständige Roadmap →' : 'Full roadmap →'}
|
||||
</Link>
|
||||
</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.nis2.cta.heading}</h2>
|
||||
<p className="text-sm text-slate-400 leading-relaxed mb-6 max-w-md mx-auto">{t.nis2.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.nis2.cta.button}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CtaBand
|
||||
title={t.personas.cta.title}
|
||||
body={t.personas.cta.body}
|
||||
action={<ButtonLink to="/pricing">{t.personas.cta.button}</ButtonLink>}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user