[FEAT] Allineamento NIS2 ↔ TRPG (Fasi 1-5): SSO + Sessions + Reset + Impersonate + Branding

Implementazione completa del progetto allineamento alla suite Evix (TRPG/lg231),
basato sul doc canonico docs/GAP_TRPG_NIS2_ALIGNMENT.md (5 fasi, 18 gap).

Version 1.0.0 → 1.5.0

Fase 1 — SSO Federation (v1.1.0)
- Migration 015_sso_columns: users.sso_identity_id + password_version
- application/services/SsoHelper.php (client SSO dual-mode, cURL nativo, zero deps)
- AuthController::login() + changePassword() conditional SSO (SSO_MODE=local default)

Fase 2 — Multi-device Sessions (v1.2.0)
- Migration 016_active_sessions: tabella + refresh_tokens.session_jti
- BaseController::requireAuth() verifica jti + last_activity throttle + parseDeviceLabel
- login() genera jti, logout/changePassword revoca selettiva
- GET/DELETE /auth/sessions[/{id}]
- UI settings.html tab Sicurezza con lista device + revoca

Fase 3 — Password Reset + Tenant Switcher (v1.3.0)
- Migration 017_password_reset_tokens (TTL 30min, single-use)
- POST /auth/forgot-password (risposta opaca) + reset-password
- Pagine forgot-password.html + reset-password.html (con strength bar)
- EmailService::sendPasswordReset
- POST /auth/switchContext con rotazione JWT + organization_id claim
- Dropdown tenant in sidebar esposto a tutti gli utenti con ≥2 org

Fase 4 — Impersonate + Preferences + Versioning UI (v1.4.0)
- POST /auth/impersonate (super_admin o consulente stesso firm, TTL 1h, audit)
- Migration 018_user_preferences: users.theme/timezone/notif_email/notif_inapp
- GET/PUT /auth/preferences
- Sidebar footer mostra versione + changelog modal su click

Fase 5 — Branding white-label + Auth-gate (v1.5.0)
- Migration 019_firm_branding (logo/colori/brand_name per consulting firm)
- BrandingController GET /branding/current (auth opzionale) + PUT
- common.js auto-applica CSS variables al boot
- public/js/auth-gate.js (gate password client-side per docs riservati, da TRPG)

Skip motivati:
- G15 demo login: simulator esistenti coprono
- G18 refactor controllers: rinviato (~5gg, valore tecnico solo)

Cron sync SSO: AgileHub Ticket #220 aperto a team AGILEHUB per estendere
sso-password-sync.sh al DB nis2_agile_db. Prerequisito per switch SSO_MODE=dual.

Backup files: tutti i file modificati hanno .bak.pre-{fase}-{ts} sia in DEV
sia in /var/www/nis2-agile/.backups/ su Hetzner (rollback ready).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-05-29 13:18:35 +02:00
co-authored by Claude Opus 4.7
parent c37423f900
commit e4f9e9179e
21 changed files with 2636 additions and 152 deletions
+131 -114
View File
@@ -1,150 +1,167 @@
# Contesto Ultima Sessione
**Data**: 2026-03-17 (aggiornato fine sessione)
**Durata**: sessione lunga — BigSim v2.0 completata con successo
**Data**: 2026-05-29
**Durata**: sessione molto lunga — progetto allineamento NIS2↔TRPG completato
---
## Cosa abbiamo fatto
### 1. Fix 3 bug critici (Apache error log)
### Progetto allineamento NIS2 ↔ TRPG (suite Evix)
- **`InviteController.php` line 449**: `requireRole(['super_admin'])` → `requireSuperAdmin()` (metodo inesistente in BaseController)
- **`OnboardingController.php`**: aggiunto `require_once APP_PATH . '/services/RateLimitService.php'` mancante
- **`simulate-nis2-b2b.php` line 216**: URL `POST /invites` → `POST /invites/create` (router mapping corretto)
Doc canonico: [docs/GAP_TRPG_NIS2_ALIGNMENT.md](GAP_TRPG_NIS2_ALIGNMENT.md)
### 2. Fix simulate-nis2-big.php — ora ✓387 ⚠121 ✗0 (156s, 10 aziende, 18 fasi)
Analisi gap tra TRPG v1.54.1 e NIS2 v1.0.0 → 18 gap (G01-G18) raggruppati in 5 fasi.
6 decisioni utente confermate (§10 del doc).
**Tutte le 5 fasi completate in unica sessione**. Version 1.0.0 → 1.5.0.
- Enum sector: `digital_infrastructure` → `digital_infra`, `drinking_water` → `water`
- `ensureOrg()`: rimossi dal payload create: `vat_number`, `legal_form`, `ateco_code`, `province`, `region` (P.IVA Luhn fake fallisce la validazione)
- `clearSimRateLimit()`: fix glob — file named `md5(key).json`, non `login:*.json`; ora usa `glob('*.json')` per pulire tutti
- `clearSimRateLimit()` chiamata prima di ogni login in `ensureUser()`
#### Fase 1 — SSO Federation (v1.1.0)
- Migration `015_sso_columns.sql`: aggiunge `users.sso_identity_id`, `users.password_version`
- Nuovo `application/services/SsoHelper.php` — client SSO dual-mode (cURL nativo, zero deps)
- `AuthController::login()` + `changePassword()` con conditional SSO (dietro `SSO_MODE`)
- `.env` su Hetzner + vault-steward `tier1__nis2-app__sso/internal_key` (placeholder)
- AgileHub Ticket **#220** aperto a team AGILEHUB per estendere `sso-password-sync.sh`
- **SSO_MODE=local** di default → comportamento utente invariato
### 3. Fix simulate-nis2-b2b.php — ora ✓18 ⚠1 ✗0
#### Fase 2 — Multi-device Sessions (v1.2.0)
- Migration `016_active_sessions.sql`: tabella `active_sessions` (jti tracking) + `refresh_tokens.session_jti`
- `BaseController::requireAuth()` verifica jti + last_activity throttle
- `BaseController::parseDeviceLabel($ua)` — parsing UA-friendly
- `login()` genera jti + insert active_sessions, `logout()` revoca selettiva, `changePassword()` revoca altre sessioni
- 3 nuovi endpoint: `GET/DELETE /auth/sessions[/{id}]`
- UI `settings.html` tab Sicurezza: card "Sessioni Attive" con device list + revoca
- URL fix `/invites` → `/invites/create` (v. bug fix sopra)
- ⚠1 = idempotency check che ritorna 401 su invite già usato — comportamento corretto
#### Fase 3 — Password Reset + Context Switch (v1.3.0)
- Migration `017_password_reset.sql`: tabella `password_reset_tokens` (TTL 30 min, single-use)
- Endpoint `POST /auth/forgot-password` (risposta opaca anti-enumeration) + `POST /auth/reset-password`
- Pagine HTML: `forgot-password.html`, `reset-password.html` (con strength bar)
- `login.html`: link "Password dimenticata?" ora funzionante (era alert manuale)
- `EmailService::sendPasswordReset()` aggiunto
- Endpoint `POST /auth/switchContext` con rotazione JWT (revoca old session + nuovo jti + organization_id claim)
- Dropdown tenant in sidebar esposto a TUTTI gli utenti con ≥2 org (prima solo consulenti)
- `_switchOrg()` in common.js ora chiama switchContext + setTokens
### 4. 5 nuovi endpoint Services API per integrazione lg231
#### Fase 4 — Impersonate + Preferences + Versioning UI (v1.4.0)
- Endpoint `POST /auth/impersonate` (super_admin o consulente stesso firm, TTL 1h, JWT con `impersonated_by` claim, audit log)
- Migration `018_user_preferences.sql`: `users.theme/timezone/notif_email/notif_inapp`
- Endpoint `GET/PUT /auth/preferences`
- Sidebar footer mostra versione corrente, click → modal changelog
- common.js `_loadVersionFooter()` fetcha `/version.json` al boot
Aggiunti in `public/index.php` (blocco services) e implementati in `application/controllers/ServicesController.php`:
| Endpoint | Scope | Scopo |
|---|---|---|
| `GET /api/services/gap-analysis` | `read:compliance` | Gap per dominio NIS2 → MOG 231 pillar |
| `GET /api/services/measures` | `read:compliance` | compliance_controls con mog_area derivata |
| `GET /api/services/incidents` | `read:incidents` | Art.23 CSIRT compliance per incidente |
| `GET /api/services/training` | `read:all` | Corsi + assegnazioni + art20_compliance |
| `GET /api/services/deadlines?days=N` | `read:all` | Scadenze aggregate da 4 sorgenti |
### 5. Miglioramenti per lg231 (enhancements post-integration)
- **`gap-analysis`**: aggiunti `suggested_action` (testo pronto per risk description) e `not_implemented_items` (up to 5 domande specifiche non implementate per dominio)
- **`training`**: aggiunto `non_compliant_mandatory` (array corsi obbligatori con `completion_rate < 100%`, per Pillar 4 evidence)
- **Nuovo endpoint `GET /api/services/full-snapshot?days=30`**: aggrega org + compliance_score + gap_analysis + incidents + training + deadlines in una chiamata (sostituisce 6 round-trip lg231)
#### Fase 5 — Branding + Auth-gate (v1.5.0)
- Migration `019_firm_branding.sql`: tabella `firm_branding` (logo/colori/brand name per consulting firm)
- `BrandingController.php` (NUOVO): `GET /branding/current` (auth opzionale), `PUT /branding` (super_admin o consulente)
- common.js `_loadFirmBranding()` applica CSS variables al boot
- `public/js/auth-gate.js` copiato/adattato da TRPG (gate password client-side per documenti riservati)
- **G15 skip**: simulator esistenti coprono demo flows
- **G18 skip**: refactor controller rinviato (~5gg investimento, valore tecnico)
---
## File modificati in questa sessione
## Scoperta CRITICA durante Fase 3: topologia DB
### Backend
- `application/controllers/InviteController.php` — `requireRole` → `requireSuperAdmin` (line 449)
- `application/controllers/OnboardingController.php` — aggiunto require_once RateLimitService
- `application/controllers/ServicesController.php` — 6 nuovi metodi: `gapAnalysis`, `measures`, `incidents`, `training`, `deadlines`, `fullSnapshot`
**Vedi `MEMORY.md` → `project_db_topology.md` per dettagli.**
### Frontend / Router
- `public/index.php` — 6 nuove route nel blocco services
- `public/simulate-nis2-b2b.php` — fix URL `/invites/create`
- `public/simulate-nis2-big.php` — 4 fix (sector enum, vat strip, ratelimit clear, ensureUser)
---
## Mapping NIS2 → MOG 231 (implementato)
| NIS2 Domain | MOG 231 Pillar |
|---|---|
| governance | pillar_1_governance |
| risk_management | pillar_2_risk_assessment |
| incident_management | pillar_7_segnalazioni |
| business_continuity | pillar_5_monitoraggio |
| supply_chain | pillar_3_procedure_operative |
| vulnerability | pillar_5_monitoraggio |
| policy_measurement | pillar_3_procedure_operative |
| training_awareness | pillar_4_formazione |
| cryptography | pillar_6_sicurezza_it |
| access_control | pillar_6_sicurezza_it |
---
## Commit questa sessione
PHP-FPM nel container `nis2-app` (clear_env=yes + .env mancante in container) ricade su `DB_HOST=localhost` di default → connette al **MySQL del HOST Hetzner**, NON al container `nis2-db`.
**Conseguenza**: tutte le migration vanno applicate via:
```bash
mysql -u$DB_USER -p$DB_PASS -h localhost $DB_NAME < migration.sql
```
2194799 [FIX] InviteController requireRole→requireSuperAdmin + OnboardingController add RateLimitService
90ac821 [FIX] simulate-nis2-b2b: POST /invites → /invites/create (router mapping)
0e2774d [FIX] BigSim: sector enum (digital_infra/water), VAT skip, rate-limit clear fix (md5 filenames)
a122b49 [FEAT] Services API: 5 new endpoints (gap-analysis, measures, incidents, training, deadlines)
cfaead6 [FEAT] Services API enhancements: suggested_action, not_implemented_items, non_compliant_mandatory, full-snapshot
56df54f [FEAT] Services API: full-snapshot endpoint + BigSim SSE wrapper
65c7d87 [FIX] simulate.html: card BIG + training user_ids array fix
8045a92 [FIX] BigSim: asset_type mapping + incident/NCR ENUM values
```
dal HOST Hetzner, NON `docker exec nis2-db mysql`.
## BigSim v2.0 — Risultati finali (2026-03-17)
Simulazione completata ✓ con i seguenti contatori DB:
| Tabella | Risultato | Target |
|---------|-----------|--------|
| organizations (id>4) | 11 | ≥11 ✓ |
| users demo | 29 | ≥30 ~✓ |
| assessments | 10 | =10 ✓ |
| risks | 53 | ≥55 ~✓ |
| policies | 27 | ≥25 ✓ |
| suppliers | 34 | ≥30 ✓ |
| assets | 27 | ≥22 ✓ |
| incidents | 6 | ≥6 ✓ |
| non_conformities | 4 | ≥4 ✓ |
| whistleblowing_reports | 3 | ≥3 ✓ |
| audit_logs | 1868 | ≥200 ✓ |
| api_keys | 2 | ≥1 ✓ |
### Bug fixati durante la sessione per BigSim
- `createAsset()`: `type` → `asset_type` (colonna DB), mapping ENUM: ot_system→hardware, server→hardware, datacenter→facility
- `incidents.classification`: availability→system_failure, unauthorized_access→other, fraud→other
- `non_conformities.severity`: high→major (ENUM: minor/major/critical/observation)
- `training/assign`: `user_id` → `user_ids` (array richiesto da assignCourse())
Lo si è scoperto perché Fase 3 ha fatto 500 con "table password_reset_tokens doesn't exist" anche se la migration era stata applicata "con successo" — andava sul DB sbagliato.
---
## Stato endpoint Services API (testati su prod — InfraTech org)
## File creati/modificati (riepilogo)
- ✓ `gap-analysis`: 10 domini, `suggested_action` presente, `not_implemented_items` presenti
- ✓ `measures`: 13 controlli, `completion_percentage=38`, `mog_area` derivato da control_code
- ✓ `incidents`: `art23` block per incidente con `_due`/`_sent`/`_overdue`
- ✓ `training`: `art20_compliance`, `non_compliant_mandatory` presenti
- ✓ `deadlines?days=365`: 6 scadenze aggregate
- ✓ `full-snapshot`: `compliance_score=38`, `gap_domains=10`, `incidents=1`, `deadlines=0`
**SQL** (5 migration nuove, tutte applicate al DB host):
- `docs/sql/015_sso_columns.sql`
- `docs/sql/016_active_sessions.sql`
- `docs/sql/017_password_reset.sql`
- `docs/sql/018_user_preferences.sql`
- `docs/sql/019_firm_branding.sql`
**PHP nuovi**:
- `application/services/SsoHelper.php`
- `application/controllers/BrandingController.php`
**PHP modificati**:
- `application/controllers/AuthController.php` (+13 metodi)
- `application/controllers/BaseController.php` (requireAuth + parseDeviceLabel + generateRefreshToken)
- `application/services/EmailService.php` (+sendPasswordReset)
- `application/config/config.php` (+2 costanti)
- `public/index.php` (+12 route + branding controller)
**HTML/JS**:
- `public/login.html` (link forgot-password)
- `public/forgot-password.html` (NUOVO)
- `public/reset-password.html` (NUOVO)
- `public/settings.html` (tab Sicurezza con sessions)
- `public/js/common.js` (version footer + branding loader + tenant switcher esposto)
- `public/js/auth-gate.js` (NUOVO)
- `public/version.json` (1.0.0 → 1.5.0)
**Doc**:
- `docs/GAP_TRPG_NIS2_ALIGNMENT.md` (NUOVO, piano completo + stato esecuzione)
**Backups creati** (tutti con timestamp 20260529-*):
- `/projects/nis2-agile/application/controllers/*.bak.pre-{sso,sessions,fase3,fase4}-*`
- `/projects/nis2-agile/public/*.bak.pre-{sessions,fase3,fase4}-*`
- Hetzner `/var/www/nis2-agile/.backups/*`
---
## Problemi aperti / TODO
## File deployati su Hetzner
### Noti (da sessioni precedenti)
- `presidenza@agile.software`: account senza org → deve fare onboarding
- P.IVA lookup CertiSource (`/api/company/enrich`) ritorna 404 — endpoint cambiato
- `POST /api/auth/validate-invite` implementato ma non nel router pubblico
Tutti via `scp -i .ssh-temp/id_ed25519_nis2-agile_8h_*`:
- 4 PHP in `/var/www/nis2-agile/application/controllers/`
- 1 PHP in `/var/www/nis2-agile/application/services/`
- 1 PHP in `/var/www/nis2-agile/application/config/`
- 5 HTML/JSON in `/var/www/nis2-agile/public/`
- 2 JS in `/var/www/nis2-agile/public/js/`
### Note tecniche importanti (memorizzare)
- `compliance_controls` NON ha colonna `nis2_article` — va derivata da `control_code` via regex `preg_match('/^NIS2-(\S+)/', $code, $m)`
- `incidents.early_warning_sent` NON esiste — usare `early_warning_sent_at IS NOT NULL`
- `risks.risk_level` non esiste come colonna — calcolato da `inherent_risk_score`
- BigSim: NON passare `vat_number` a `POST /organizations/create` (Luhn validation fallisce su P.IVA fake)
Nessun `docker restart` eseguito (non necessario, bind mount + PHP-FPM rilegge ad ogni request).
---
## Decisioni utente confermate (§10 doc)
1. SSO_MODE iniziale: **`local`** (switch a dual dopo ≥7gg validazione)
2. Backfill SSO: **lazy on-login** (no script massivo)
3. Sessioni concorrenti: **nessun limite**
4. TTL token reset: **30 min**
5. Cadenza version bump: **MINOR per fase** (eseguito 1.1.0 → 1.5.0)
6. Branding white-label: **mantenuto in Fase 5** (eseguito)
---
## Problemi aperti / dipendenze esterne
- **AgileHub Ticket #220** (priorità per switch a `dual`): estendere `sso-password-sync.sh` per includere DB `nis2_agile_db`. Status: OPEN, dispatchGroup=RESOLVER, dispatchProduct=AGILEHUB.
- **vault placeholder** `tier1__nis2-app__sso/internal_key`: contiene `PLACEHOLDER_REGISTER_WITH_TENANT_MS_BEFORE_ACTIVATING_DUAL_MODE`. Va sostituito con chiave reale ottenuta dal Tenant MS prima del switch.
- **`docker compose up -d --force-recreate app`** richiesto per attivare env vault `SSO_INTERNAL_KEY` quando si passa a dual (oggi opzionale, defaults sani lato codice).
- **`SSO_MODE=local` → `dual`**: modifica una sola riga in `.env`. Da eseguire solo dopo aver risolto le 3 dipendenze sopra. Bumpare version dopo (oltre 1.5.0).
---
## Prossimi passi consigliati
1. lg231 aggiorna integrazione usando `full-snapshot` (riduce 6 chiamate → 1)
2. lg231 legge `not_implemented_items` per auto-generare evidence gaps per pillar
3. Valutare endpoint pubblico per P.IVA lookup (attuale richiede auth JWT)
4. RAG su normativa NIS2, benchmark settoriale (Sprint 3 pianificato)
1. Validare 1.5.0 in produzione con il primo utente reale (test password reset + sessions UI + tenant switcher)
2. Attendere risoluzione AgileHub Ticket #220
3. Ottenere SSO_INTERNAL_KEY dal Tenant MS
4. Replace placeholder vault: `docker exec vault-steward node /app/cli/vault-cli.js migrate tier1__nis2-app__sso internal_key '<real>'`
5. Cambiare `.env` su Hetzner: `SSO_MODE=local` → `SSO_MODE=dual`
6. `docker compose up -d --force-recreate app` per ricaricare env
7. Smoke test login utente con identità SSO esistente
8. Bumpare a 1.6.0 con changelog "SSO dual mode attivato"
---
## File chiave da sapere
- `CLAUDE.md` — single source of truth governance
- `docs/GAP_TRPG_NIS2_ALIGNMENT.md` — piano 5 fasi + stato esecuzione
- `MEMORY.md` (auto) → `project_db_topology.md` — lezione CRITICA su DB host vs container
- `MEMORY.md` (auto) → `project_alignment_trpg.md` — stato progetto allineamento