NIS2 Compliance-Officer End-to-End Scenarios
| Status | Design + live-stack Playwright authoring (QA / Test Automation) |
| Owner | Test Automation + NIS2 compliance specialist |
| Pack | NIS2 Evidence Pack — claim boundary operator-support |
| Audience | QA authors writing the live-stack specs, and product/compliance reviewers validating the operator-support framing |
Purpose
This document captures realistic, end-to-end scenarios written from the point of view of a NIS2-obligated compliance officer who uses the Stella Ops NIS2 Evidence Pack to author a Statement of Applicability (SoA) control register, publish a signed SoA bundle, track Article 23 incident milestones, and review thirteen-area effectiveness evidence.
The companion live-stack Playwright specs author (do not run — a later wave executes them one by one):
src/Web/StellaOps.Web/tests/e2e/compliance/nis2-control-register.e2e.spec.tssrc/Web/StellaOps.Web/tests/e2e/compliance/nis2-soa-export.e2e.spec.tssrc/Web/StellaOps.Web/tests/e2e/compliance/nis2-article23-timeline.e2e.spec.tssrc/Web/StellaOps.Web/tests/e2e/compliance/nis2-effectiveness-dashboard.e2e.spec.ts- fixture:
src/Web/StellaOps.Web/tests/e2e/compliance/live-auth.fixture.ts
Claim boundary (forbidden vs. approved copy)
Per docs/compliance/README.mdthe pack is operator-support: Stella helps the customer collect, review, and export evidence; the customer remains the regulated decision-maker. The scenarios and specs MUST NOT assert or imply “Stella is NIS2 compliant”. Approved framing is “NIS2 evidence support”.
Thematic areas 8 (cyber hygiene/training), 10 (HR security) and 13 (environmental/physical security) are not Stella-native telemetry domains. They must stay in the register with explicit applicability: not-applicable and reason not-applicable-to-stella-native-telemetry, backed by tenant-supplied evidence pointers. The authoring form defaults areas 8/10/13 to not-applicable with that reason (nis2-control-author-form.component.ts DEFAULT_NOT_APPLICABLE_AREAS).
System facts the scenarios rely on (verified against src/)
Console routes (Web)
- Control register:
/assurance/nis2/control-register(alias/compliance/nis2/control-register) →Nis2ControlRegisterPageComponent. - Incident timelines:
/assurance/nis2/incidents→Nis2IncidentDashboardPageComponent. - Effectiveness:
/assurance/nis2/effectiveness→Nis2EffectivenessDashboardPageComponent. - Assurance shell:
/assurance→AssuranceFrameworksPageComponent. - Route guards: control register / effectiveness require
policy:readorpolicy:audit; incidents requirenotify.viewer,findings:read, orpolicy:audit.
Gateway-fronted API routes (what the live UI + request calls hit)
| Purpose | Gateway path | Service path | Scope |
|---|---|---|---|
| List NIS2 register | GET /api/policy/control-register/nis2 | Policy …/api/policy/control-register/nis2 | policy:read |
| Author NIS2 control | POST /api/policy/control-register/nis2 | Policy | policy:author |
| Archive NIS2 control | DELETE /api/policy/control-register/nis2/{controlId} | Policy | policy:author |
| Framework projection | GET /api/policy/control-register/frameworks/nis2 | Policy | policy:read |
| SoA export profile | GET /api/export-center/v1/exports/nis2/soa/profile | ExportCenter /v1/exports/nis2/soa/profile | export:read (ExportViewer) |
| Start SoA run | POST /api/export-center/v1/exports/nis2/soa/runs | ExportCenter | export:operate (ExportOperator) |
| Get SoA run | GET /api/export-center/v1/exports/nis2/soa/runs/{runId} | ExportCenter | export:read |
| Download SoA bundle | GET /api/export-center/v1/exports/nis2/soa/runs/{runId}/bundle | ExportCenter | export:read |
| Assurance export profiles | GET /api/export-center/v1/exports/assurance/profiles | ExportCenter | export:read |
| Article 23 timeline profile | GET /api/v1/assurance/reporting-timeline-profiles/nis2.article23.incident | Notify | notify.viewer |
| Article 23 profile readiness | GET /api/v1/assurance/reporting-timeline-profiles/nis2.article23.incident/readiness | Notify | notify.viewer |
| Persisted incident timelines | GET /api/v1/regulatory/reporting-timelines?regime=nis2 | Notify | notify.viewer |
Source of the gateway routing: src/Router/StellaOps.Gateway.WebService/appsettings.json (^/api/(cvss|gate|exceptions|policy)(.*) → policy-engine; ^/api/export-center/v1/exports… → exportcenter; ^/api/v1/assurance/reporting-timeline-profiles… → notify).
Deterministic hashes and identifiers
- Control mapping hash: each authored/updated control returns
mappingHash(sha256:…) in thePOST …/nis2response and again in theGET …/nis2controls[].mappingHash. Re-authoring an identical control yields the samemappingHash(the binder canonicalizes); editing a field changes it. (ControlRegisterEntryRecord.MappingHash, surfaced byNis2ControlRegisterEndpoints.ToControl.) - Snapshot / SoA hashes: a completed SoA run returns
bundleHash,soaExportHash,controlRegisterSnapshotHash(Nis2SoaRunResponse).controlRegisterSnapshotHashis computed byNis2ControlRegisterSnapshotBuilder.ComputeHashover the canonical, area+id-sorted snapshot — so identical register state ⇒ identical snapshot hash. The run id itself is content-addressed (DeriveGuid("nis2-soa-run", tenant, snapshotHash, since, evidenceRef, redact)), so an identical export request is idempotent and returns the samerunId. - Audit event ids: register lifecycle events (
control:registered/updated/archived/snapshot_published) are surfaced per control ascontrols[].auditEventIds[](sequence-ordered Policy event ids). - Article 23 milestones: profile fixes
early-warningduePT24H,notificationduePT72H,final-reportdueP1M;signingRequired:true;payloadContract: nis2-incident-report-envelope-v1; channelsnis2-csirt.de/fr/nl,signed-json-webhook,pgp-email. (AssuranceReportingProfileRegistry.BuildNis2Profile.)
Readiness / fail-closed posture (important for assertions)
- The SoA export is fail-closed:
Nis2SoaExportServicethrowsNotSupportedExceptionwhenExport:Nis2Soa:SigningKeyId,TrustRoots, orStorageRootare not configured. On a stack without production signing config,POST …/soa/runsreturns a 4xx/5xx and the Console surfaces an “export setup required” notice (nis2-export-setup-notice) instead of fabricating evidence. The spec asserts either a completed signed run (when configured) or a clean fail-closed signal (never a fake bundle). This is the operator-support / no-fabrication boundary in action. - The Console SoA button (
nis2-export-soa-button) is disabled unless the register loaded live Policy data with ≥1 control and ageneratedAt(exportBlockedReason()); fixture-mode (API down) keeps it disabled.
Scenario 1 — Author the SoA control register
Compliance-officer narrative. “I am preparing my organisation’s NIS2 Statement of Applicability. For each of the thirteen Article 21 thematic areas I record the controls we operate, who owns them, and where the evidence lives. Some controls are backed by Stella-native telemetry (gates, scans, evidence capsules); others — supplier attestations, HR/training, physical security — are tenant-supplied evidence I attach by reference. For areas 8, 10 and 13 I record an explicit not-applicable to Stella-native telemetry decision because Stella does not run my workforce or facilities.”
Flow.
- Sign in to Console (admin), open
/assurance/nis2/control-register. - Confirm the page shows Live Policy data (not contract-fixture) — chip + absence of the
result.source === 'contract-fixture'warning. (If the stack has no register rows yet, authoring in this scenario creates them.) - Author an applicable, Stella-native area-6 control (secure development gate) via the author form —
nis2-author-control-button→ form →nis2-author-submit. - Author an applicable, tenant-supplied area-5 supplier-evidence control with a
tenant-evidence://…pointer andsubmittedByRole. - Author a not-applicable area-10 (HR security) control; assert the form pre-fills
applicability=not-applicable+ reasonnot-applicable-to-stella-native-telemetry. - Reload the register; confirm each control appears with its
mappingHash, area name, evidence mode, and audit event ids.
Concrete UI + API.
- UI:
Nis2ControlRegisterPageComponentauthor form (testids:nis2-author-control-id,nis2-author-thematic-area,nis2-author-responsible-role,nis2-author-policy-doc-ref,nis2-author-art21-refs,nis2-author-applicability,nis2-author-applicability-reason,nis2-author-add-evidence, evidence sourcenis2-author-evidence-source-{i},nis2-author-submit). Success bannernis2-author-successshows the registered controlId +mappingHash. - API equivalents (driven via
requestfor deterministic JSON assertions):POST /api/policy/control-register/nis2with a Policy document body, thenGET /api/policy/control-register/nis2.
Expected assertions.
POSTresponse:sourceOfTruth === "Policy",registered === 1,controls[0].mappingHashmatches^sha256:[0-9a-f]{64}$,controls[0].status === "registered"(first write) andevidenceModederived (stella-native/tenant-supplied/mixed).- Determinism: re-
POSTthe identical area-6 document ⇒status === "updated"(re-register) and the samemappingHash(canonical, idempotent). ChangenextReview⇒ differentmappingHash. GETresponse: exactly 13thematicAreas[]; the authored controls appear under their area; area-10 control carriesmetadata.applicability === "not-applicable"andmetadata.applicabilityReason === "not-applicable-to-stella-native-telemetry"; tenant-supplied control’s pointersourceType === "tenant-supplied"andevidenceMode !== "stella-native"; each authored control has ≥1auditEventIds[].- Claim-boundary: page copy never says “compliant”; the author-form note explicitly states “records your organisation’s NIS2 control posture; it is not a compliance certification”.
Scenario 2 — Publish a signed Statement of Applicability
Compliance-officer narrative. “Once my register is complete for all thirteen areas, I publish a signed, deterministic SoA bundle that my auditor (or the competent authority on handoff) can verify offline. The bundle pins the control register snapshot hash and the SoA export hash. If my signing trust roots or ExportCenter storage aren’t configured, the system must block me — it must never hand me a bundle that looks signed but isn’t.”
Flow.
- From the control register (live data), click Export SoA (
nis2-export-soa-button). - The Console fetches the profile (
/exports/nis2/soa/profile), confirmsstatus === "active", thenPOST …/soa/runswith acontrolRegisterSnapshotbuilt from the live register. - The SoA panel (
nis2-soa-export-status) shows the job/run ids, the bundle hash and SoA hash, progress, and a signed-bundle link. - (API) Download the bundle from
…/soa/runs/{runId}/bundleand assert the content type + that the run’s hashes are present and stable.
Concrete UI + API.
- UI:
nis2-export-soa-button(disabled unless live + ≥1 control), panelnis2-soa-export-statusshowingBundle hashandSoA hashcells; setup noticenis2-export-setup-noticewhen blocked. - API:
GET /api/export-center/v1/exports/nis2/soa/profile→{ profileId, status:"active", schemaVersion };POST /api/export-center/v1/exports/nis2/soa/runs→Nis2SoaRunResponse;GET …/runs/{runId}and…/runs/{runId}/bundle.
Expected assertions.
- Profile:
status === "active",profileId === "nis2:statement-of-applicability",requiredScope === "export:operate". - Configured path (signing + trust roots + storage present): run
statusreachescompleted;bundleHash,soaExportHash,controlRegisterSnapshotHasheach match^sha256:…;verification.isValid === truewith averifiedKeyId/verifiedTrustRootId; bundle download content typeapplication/vnd.stellaops.nis2.soa.bundle.v1+json. Re-POSTidentical request ⇒ samerunId(content-addressed idempotency) and identical hashes. - Fail-closed path (production signing not configured):
POST …/soa/runsreturns 4xx/5xx and the Console rendersnis2-export-setup-notice(“Signed SoA export setup required”) and points at the Assurance setup step. No bundle, no fake hashes. This is the certifiable-evidence guarantee: the system refuses to emit unsigned “evidence”. - Completeness gate (publish-snapshot UI, shipping concurrently in the same component dir): when an area decision is missing, publish must be blocked. The spec checks for the gate signal if present and otherwise records it as a pending-UI gap (see Gaps), without editing the concurrent component files.
- Claim-boundary: SoA docs/UI frame the bundle as “operator-owned export for review, audit, or handoff” — never a Stella certification.
Scenario 3 — Article 23 incident reporting timeline
Compliance-officer narrative. “If we suffer a significant incident, NIS2 Article 23 puts me on a clock: an early warning within 24 hours, a full notification within 72 hours, and a final report within one month. Stella gives me a signed, replayable timeline whose source of truth is Notify, so I can prove to the authority exactly when each milestone was met.”
Flow.
- Open
/assurance/nis2/incidents; confirm the page renders (nis2-incident-dashboard-page) and links back to the control register. - (API) Fetch the profile
…/reporting-timeline-profiles/nis2.article23.incident; assert the three milestone definitions and their due windows. - (API) Fetch profile readiness
…/nis2.article23.incident/readiness; assert stable reason codes and that no secret values are exposed. - (API) Fetch persisted timelines
…/regulatory/reporting-timelines?regime=nis2; for any present timeline, assert milestone names/states, deadline statuses, replay hash, and signed transmissions.
Concrete UI + API.
- UI:
Nis2IncidentDashboardPageComponent(nis2-incident-dashboard-page), reads/api/v1/regulatory/reporting-timelines?regime=nis2. - API:
GET /api/v1/assurance/reporting-timeline-profiles/nis2.article23.incident(+/readiness),GET /api/v1/regulatory/reporting-timelines?regime=nis2.
Expected assertions.
- Profile:
timelineProfileId === "nis2.article23.incident",frameworkId === "nis2",packId === "nis2",claimBoundary === "operator-support",signingRequired === true,payloadContract === "nis2-incident-report-envelope-v1";milestones[]containsearly-warning(dueWithin:"PT24H"),notification(PT72H),final-report(P1M); every milestonesignatureRequired === true; channels includenis2-csirt.de,signed-json-webhook,pgp-email. - Readiness:
secretValuesExposed === false;status∈ {configured,blocked}; reason codes are from the documented stable set (configured,csirt-destination-missing,operator-approval-missing,auto-submit-disabled, …);autoSubmitReadyisfalseunless explicitly enabled (default-disabled posture). - Persisted timelines:
sourceOfTruth === "Notify". On a fresh stack the list may be empty — the spec asserts the contract shape and, when ≥1 timeline exists, that each milestone has adeadlineStatus, the item carries areplayHash, and anytransmissions[]carrypayloadHash+signerRef(signed + replayable). Driving milestones is not an HTTP operation (the regulatory route is read-only; timelines are populated by Notify’s incident pipeline / QA fixture import), so this is recorded as a gap, not a fabricated POST. - Claim-boundary: the reporting party is the operator; default behaviour is a signed handoff artifact, not automatic regulator submission.
Scenario 4 — Thirteen-area effectiveness (KPI) dashboard
Compliance-officer narrative. “Between audits I watch a KPI dashboard that scores effectiveness across all thirteen thematic areas, so I can spot a control whose evidence is going stale before my auditor does.”
Flow.
- Open
/assurance/nis2/effectiveness; confirm the page renders (nis2-effectiveness-dashboard-page). - Assert per-area effectiveness cards render (
nis2-effectiveness-area-{n}) and that the view links back to the control register and incidents.
Concrete UI + API.
- UI:
Nis2EffectivenessDashboardPageComponent(nis2-effectiveness-dashboard-page), reads/api/telemetry/nis2/effectiveness.
Expected assertions.
- The dashboard page renders; if telemetry is available, the thirteen-area cards (
nis2-effectiveness-area-1..13) render; otherwise the page shows an explicit empty/blocked state rather than fabricated KPI numbers (tenant-supplied / blocked areas stay explicit). - Claim-boundary: the dashboard is “an operator evidence and review aid; it does not replace the customer’s management review or auditor judgement”.
How to run (later wave — do NOT run now)
The specs target the live Docker stack at https://stella-ops.local (ignoreHTTPSErrors) and carry their own live OIDC login + Bearer-token API context, modelled on the live-deployment specs that scripts/run-live-deployment-vm-e2e.mjs runs via --config.
Config note (verified): the existing live-Docker config playwright.e2e.config.ts has testDir: './e2e', so it does not discover specs placed under tests/e2e/compliance/ (Playwright filters positional spec paths against testDir). A dedicated live config was therefore added — playwright.nis2-compliance.config.ts (testDir tests/e2e/compliance, live baseURL, no webServer), mirroring playwright.airgap-federation.config.ts. npx playwright test --config playwright.nis2-compliance.config.ts --list discovers all 12 tests across the 4 files.
# from src/Web/StellaOps.Web, with the docker compose stack up at https://stella-ops.local
$env:STELLAOPS_ADMIN_USER = "admin"
$env:STELLAOPS_ADMIN_PASS = "<the password your installer printed>"
npx playwright test --config playwright.nis2-compliance.config.ts
# filter one scenario:
npx playwright test --config playwright.nis2-compliance.config.ts -g "@nis2-soa"
Scenario tags (on test.describe): @nis2-control-register, @nis2-soa, @nis2-article23, @nis2-effectiveness.
Known gaps (report only — do not fix)
- No HTTP path to open/advance an Article 23 timeline.
…/regulatory/reporting-timelinesis read-only; live timelines come from Notify’s incident pipeline or the (deprecated/soak) QA fixture-import endpoint/api/v1/qa/fixtures/advanced-assurance-golden/notify-import(and the newerstellaops-testerservice). Scenario 3 therefore verifies the profile + readiness contract and asserts persisted-timeline shape when present; full milestone-driving needs the incident pipeline or fixture seed. - SoA export depends on production signing config. Without
Export:Nis2Soa:SigningKeyId/TrustRoots/StorageRootthe run fails closed. The local compose stack may not ship these, so the signed-bundle assertions are guarded (configured vs. fail-closed branch). Verifying the configured happy-path needs those options set in the deployment. - Publish-snapshot + 13-area completeness gate UI is landing concurrently (SPRINT_20260530_020, same component dir). Its exact testids are not yet stable; Scenario 2’s completeness-gate assertion probes defensively and records the gate as pending if the control isn’t present, without touching the shared component files.
- Effectiveness KPI telemetry source (
/api/telemetry/nis2/effectiveness) may be empty on a stack without seeded KPI data; the area-card assertions are guarded behind data presence.
