nis2-agile/docs/CONTEXT_LAST_SESSION.md
DevEnv nis2-agile db4cc7f660 [DOCS] CONTEXT: sessione 2026-05-30 - 4 feature gap competitivi Evix (ingestion/evidence/asset import/FAIR)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 09:26:31 +02:00

261 lines
20 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Contesto Ultima Sessione
> Il 2026-05-29 ci sono state DUE sessioni: **pomeriggio** (questa, qui sotto) e **mattina** (TRPG alignment, più in basso).
---
## SESSIONE 2026-05-30 mattina (~07:50→09:15 CEST) — Chiusura gap competitivi Evix (backend)
Obiettivo utente: "NIS2 punto di riferimento" → implementare DAVVERO le carenze vs concorrenza (Vanta/Drata/GRC) dall'analisi `docs/EVIX_ANALISI_CONCORRENZA.html`. **4 feature backend complete, testate E2E in prod, committate e pushate.**
### Feature implementate (tutte su Services API + scope dedicati)
1. **P1 Ingestion incidenti SIEM/SOC/EDR** (commit `2190999`): `POST /api/services/incidents-ingest` (scope `ingest:incidents`). Crea incidente Art.23 da alert esterno, dedup su `external_ref`, `mapSeverity` (CVSS/P1-P5/stringhe→enum), classificazione AI (IS-1..4), deadline 24h/72h/30g, webhook. Migrazione **023** (incidents += source/source_system/external_ref + UNIQUE dedup).
2. **P1 Evidence Automation + Continuous Control Monitoring** (commit `307993f`): `POST /api/services/evidence-ingest` (scope `ingest:evidence`, batch ≤200) + `GET /api/services/controls-monitoring` (scope read:compliance). Stato semaforo healthy/warning/stale/failing per freschezza. Migrazione **024** (tabella `control_evidence_auto` + compliance_controls += monitoring_status/last_checked_at/freshness_days).
3. **P2 Asset import CMDB/cloud** (commit `4924075`): `POST /api/assets/import` (JWT) + `POST /api/services/assets-ingest` (scope `ingest:assets`). `AssetScoringService::inferCriteria` (euristica 6 criteri GV.OC-04 da campi CMDB) → scoring automatico. Upsert dedup su external_ref, max 1000/batch. Migrazione **025** (assets += external_ref/discovery_source + UNIQUE).
4. **P2 Risk quantitativo FAIR + KRI** (commit `a3f2e91`): `FairService` Monte Carlo (PERT su TEF/Loss Magnitude, ALE EUR P10/P50/P90, deterministico). `POST /risks/{id}/fair`, `GET /risks/fairRegister` (portfolio ALE), KRI CRUD `GET/POST /risks/kri` + `PUT /risks/kri/{id}` con semaforo green/amber/red. Migrazione **026** (risks += params FAIR/ALE; tabella `kri`).
### UI area provider (commit `094d453`)
`public/integrazioniext.html`: sezione "Catalogo Connettori — Evidence Automation" con 8 card "In roadmap" (M365/Google/AWS/Azure/IdP/EDR/SIEM/Ticketing) + badge `.badge-roadmap`.
### API key create (org 129 Agile, prod — salvate dall'utente)
SIEM `nis2_siem_70246bbf...`, lg231 `nis2_lg231_2b6b4c09...`, SustainAI `nis2_sustai_30fb3796...`, AllRisk `nis2_allrsk_ecb9ea92...`, Evidence `nis2_evid_4cc44366...`, CMDB `nis2_cmdb_ebbc0dc8...`. (key_prefix = varchar(12)! created_by NOT NULL → uso 330.)
### ⚠️ LEZIONI CRITICHE (cambiano il workflow)
- 🔴 **OPcache `validate_timestamps=Off`**: le modifiche PHP NON sono live finché non si fa `docker exec nis2-app sh -c 'kill -USR2 1'` (reload graceful). Il bind-mount aggiorna il file ma php-fpm serve il bytecode in cache.
- 🔴 **Modifiche non committate a RISCHIO**: durante la sessione un commit esterno (`d5d83bb`, cron agent/operatore) ha fatto checkout e **revertato la mia route in index.php non committata**. → committare+pushare appena una feature è verde.
- ✅ Push su Gitea: fatto via SSH dall'host (`git push` con helper vault), il container devenv non ha il token.
### ❌ NON ancora fatto (onestà) — prossimi passi
- **UI frontend** delle 4 feature: sono backend/API. Mancano i pannelli utente (dashboard KRI, monitoraggio continuo controlli, bottone import asset CSV, form calcolo FAIR).
- `help.js` + i18n IT/EN per le nuove funzioni (regola CLAUDE.md).
- OpenAPI spec (`/api/services/openapi`) da estendere coi nuovi endpoint.
- Aggiornare card EVIX/scorecard: i gap P1/P2 ora sono chiusi a livello backend.
- Le 8 card connettori sono "In roadmap" (predisposizione UI): i connettori per-vendor reali (OAuth M365/AWS...) richiedono client+credenziali, sono fuori da questa sessione.
---
## SESSIONE POMERIGGIO — 2026-05-29 (~15:30→17:10 CEST)
### 0. Perché "tutti i prompt sono morti improvvisamente"
Il container **`nis2-agile-devenv` si è RIAVVIATO alle 15:25:55 CEST**. Prova: `uptime` (up 4 min) + `supervisord.log` (supervisord ripartito da pid 98 alle 15:27, nessun crash/respawn interno prima) → **restart esterno** (probabile reboot VM Hetzner; docker/dmesg non ispezionabili da dentro). Le sessioni Claude girano come processi DENTRO il container → terminate tutte insieme. **Nessuna perdita dati** (bind mount persistente). Il cron `ticket-agent` è ripartito normalmente (OPEN_TICKETS.md riscritto alle 15:30 → conferma ripresa, benigno). Niente OOM/disco pieno.
### 1. Messa in sicurezza sorgenti (git)
HEAD era già = `origin/main` (`4a85abe`), ma c'era MOLTO lavoro non committato — incl. **l'intero modulo RAG/KB di aprile mai entrato in git**. Creati **5 commit su `main`** (⚠️ **SOLO LOCALI, NON ancora su Gitea**):
- `1d13166` [CHORE] .gitignore: esclude `*.bak*`, `.backups/`, `.ssh-temp/` (quest'ultimo proteggeva una **chiave SSH privata**!)
- `c0bf7b6` [DOCS] standard cross-suite + governance CLAUDE.md + registri agent
- `1d934e4` [FEAT] UI: guida.html, index-en, mobile-conversion, ai-assistant, bug-reporter, help/i18n
- `9b53ca3` [FEAT] MktgLead getJsonBody + import-feedback-to-nexus + seed demo
- `a7a21fa` [FEAT] Knowledge Base RAG (KnowledgeBaseController + VectorService/EmbedService/RagService + kb.html/js + SQL 012/013 + AIService::askWithRag + qdrant in compose)
- 🔐 **Scrub sicurezza**: rimossa la **API key Voyage hardcoded** da `EmbedService.php` e `docker-compose.yml` (ora solo in `.env` gitignored + vault). NON è finita su Gitea. *(Valutare rotazione della chiave: era in chiaro su disco, condivisa con sustainai.)*
- Rimossi 2 duplicati orfani: `application/controllers/{config.php,EmailService.php}` (copie identiche dei canonici).
> ⚠️ **TODO CRITICO**: i 5 commit sono solo nel `.git` locale. Per il backup su Gitea serve `git-login` (token, cache svuotata dal reboot) + `git push origin main`. Il push tentato è fallito per assenza token. **Anche questo file (CONTEXT) e altre eventuali modifiche doc sono da committare/pushare.**
### 2. Modifiche DB PRODUZIONE — utenti/org "Agile"
Scoperto che **"Agile" = `consulting_firms` id 1 → "Agile Technology SRL"** (unico studio nel sistema). Il `super_admin` vive in **`users.role`** (enum globale), NON in `user_organizations.role`.
-**Creato utente Simon Fattori** (id **330**, `s.fattori@agile.software`): `role=org_admin`, `consulting_firm_id=1`, **utente LOCALE (no SSO, `sso_identity_id=NULL`)**, password `Fattori2026!@` impostata e verificata con `password_verify`.
-**Tagliavini (id 326)** declassato **`super_admin``org_admin`** (resta firm 1). `role` NON è sincronizzato dall'SSO → cambio stabile. Restano altri super_admin (3 admin@certisource, 4 benassati, 46 worker, 107 sim-b2b).
-**Creata organizzazione `Agile Technology SRL`** (id **129**, `entity_type=important`, `sector=ict_services`, `consulting_firm_id=1`, `voluntary_compliance=0`) per **dogfooding NIS2 su sé stessi**. org_admin: Fattori (`is_primary`) + Tagliavini. `firm_org_assignments` 7/8/9. Firm 1 ora gestisce **126/127/128/129**.
- `employee_count` e `annual_turnover_eur` lasciati NULL → da completare in onboarding (servono per soglia dimensionale NIS2).
- Tutte le scritture in **transazione** (commit OK), solo su `nis2_agile_db`.
### Accesso infrastruttura (IMPORTANTE per le prossime sessioni)
-**Dal container devenv NON si raggiunge il DB** di produzione (`nis2_user@172.18.0.7` → Access denied; `localhost` non ha MySQL qui).
-**La chiave SSH documentata `docs/credentials/hetzner_key` è ASSENTE.**
- ✅ Usata la **chiave SSH effimera `.ssh-temp/id_ed25519_nis2-agile_8h_20260529-120834`** (validità 8h da 12:08 → scade ~20:08 del 29/05) per `ssh root@135.181.149.254``mysql -h localhost -u nis2_user`. Creds DB da `/var/www/nis2-agile/.env` sul server.
### 3. Integrazione analisi `docs/nis2/` → prodotto (v1.7.0) — DEPLOYATA in produzione
L'utente ha caricato in `docs/nis2/` mockup HTML di un sistema NIS2 + 5 PDF normativi ufficiali. Integrati nel prodotto e **deployati live**. Doc completo: **`docs/nis2/INTEGRAZIONE_COMPLETATA.md`**.
- **Fase 1** Asset Relevance Scoring NIS2 0-100 a 6 criteri (GV.OC-04): `AssetScoringService.php`, endpoint `assets/scoringGrid|{id}/score|relevantSystems`, UI `assets.html`, registro stampabile (`audit/relevantSystemsRegister`). SQL `020`.
- **Fase 2** Tassonomia incidenti Determina ACN 164179/2025: IS-1..4 + `entity_obligation` essential/important (Allegati 3/4). SQL `021`, `IncidentController::create`, `AIService::classifyIncident`.
- **Fase 3** PIR 5-Whys + metriche TTD/TTC/TTR + timestamp di fase auto. SQL `022` (tab `incident_pir`), endpoint `incidents/{id}/metrics|pir`.
- **Fase 4** Mapping NIST CSF 2.0 (43 controlli) reference-only: `audit/nistCsfMapping` (no migrazione).
- **FONTI CERTE** (richiesta utente: AI+help citano fonti certe): `application/config/nis2_sources.php` (registry), `AIService::authoritativeSourcesBlock()` iniettato nei prompt (vieta riferimenti inventati), citazioni in `help.js`, **ingest PDF normativi nella KB**: `scripts/ingest-nis2-sources.php`**287 chunk in Qdrant `nis2_kb` scope SYSTEM** (retrieval verificato).
- **Analisi concorrenza Evix**: `docs/EVIX_ANALISI_CONCORRENZA.html` (gap-driven, suite=prodotti Agile, vs GRC intl + NIS2 IT).
**Eseguito su Hetzner**: backup `/root/backup_pre_v170_20260529_165447.sql`; migrazioni 020/021/022 applicate; ingest 287 chunk; smoke test endpoint (401 ok).
### ⚙️ Scoperte infrastrutturali NON OVVIE (per prossime sessioni)
- 🔑 **`/projects/nis2-agile` (devenv) e `/var/www/nis2-agile` (host) sono LO STESSO filesystem via bind mount** → le modifiche ai file sono **già live in produzione**, scp/git-pull NON servono. Confermato: stesso `git status`.
- 🐛 **Qdrant IP drift**: `nis2-qdrant` non ha IP statico in compose, era driftato `172.21.0.5`→`172.21.0.3`. L'IP era hardcoded in `VectorService.php` E in `docker-compose.yml` (`QDRANT_URL`). **Fix applicato**: entrambi → hostname `http://nis2-qdrant:6333` (drift-proof). `app` ricreato (`docker compose up -d app`), vault/chiavi preservate, RAG web verificata.
- ⚠️ **`docker.conf` ha `clear_env = no`** → php-fpm **EREDITA** le env (contraddice la vecchia nota CLAUDE.md su clear_env): per questo l'IP morto in `QDRANT_URL` rompeva la RAG web. CLI risolve l'hostname Qdrant via Docker DNS.
- `pdftotext` presente sull'host ma NON nel container `nis2-app` (alpine); Qdrant raggiungibile solo dal container (network), non dall'host. Strategia ingest: estrarre testo su host in cache `.txt`, far girare l'ingest nel container (legge `.txt`).
-**ARIA cablato**: creato `AiController::ask` (`POST /api/ai/ask`) → `AIService::askWithRag`. Il FAB ARIA (`common.js`) chiamava `/api/ai/ask` ma il controller NON esisteva → assistente AI **era rotto**, ora funziona e cita le fonti certe (verificato in prod: `rag_used=True`, sources = Ambiti/Determina). `kb_uploaded_documents` non esiste su questo DB (mig. KB 012-014 non applicate qui) → tracking MySQL KB saltato (best-effort), ma la ricerca legge da Qdrant.
- 🐛🔑 **DNS php-fpm (verificato con diagnostico fpm-fcgi reale, poi rimosso)**: nei worker php-fpm Alpine/musl, durante una request HTTP, **`getenv('QDRANT_URL')` ritorna false** E **`gethostbyname`/curl NON risolvono gli hostname Docker single-label** (`nis2-qdrant` → "Could not resolve host"). **Funziona SOLO un IP letterale** (172.21.0.3 → 200). In CLI invece getenv+gethostbyname funzionano. Perciò: `VectorService` fallback = **IP letterale `172.21.0.3`** (fpm-safe, live via bind mount); `QDRANT_URL` compose = hostname (per CLI/cron, drift-proof via gethostbyname). ⚠️ **DRIFT**: se nis2-qdrant viene ricreato e cambia IP, aggiornare il fallback in `VectorService.php`. **Fix definitivo** (non fatto, richiede recreate rete): `ipv4_address` statico per qdrant in `docker-compose.yml` (subnet 172.21.0.0/16; IP liberi es. .10). NB: `clear_env=no` in docker.conf NON basta a passare l'env ai worker (verificato getenv=false).
- Esiste un `public/_dbg_voyage.php` (debug Voyage, non mio) ancora presente in prod — valutare rimozione.
### ⚠️ TODO aperti (sessione 3)
- **PUSH Gitea**: 2 commit miei (`5c545ea` feat + `94d7867` fix Qdrant) + i 5 dell'agente sono SOLO locali. Serve `git-login` (token) + `git push origin main`. Tentato, fallito per token assente.
- **Lavoro "prossimo" da AgileHub**: l'utente l'ha menzionato ma **senza allegare contenuto/ID ticket** → da chiarire alla ripresa.
- Opzionale: cablare `askWithRag` a endpoint web; valutare IP statico Qdrant in compose; applicare migrazioni KB `kb_uploaded_documents` se serve la lista doc in UI.
---
## SESSIONE MATTINA — 2026-05-29 (TRPG alignment)
**Data**: 2026-05-29
**Durata**: sessione molto lunga — progetto allineamento NIS2↔TRPG completato
---
## Cosa abbiamo fatto
### Progetto allineamento NIS2 ↔ TRPG (suite Evix)
Doc canonico: [docs/GAP_TRPG_NIS2_ALIGNMENT.md](GAP_TRPG_NIS2_ALIGNMENT.md)
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.
#### 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
#### 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
#### 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
#### 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
#### 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)
---
## Scoperta CRITICA durante Fase 3: topologia DB
**Vedi `MEMORY.md` → `project_db_topology.md` per dettagli.**
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
```
dal HOST Hetzner, NON `docker exec nis2-db mysql`.
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.
---
## File creati/modificati (riepilogo)
**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/*`
---
## File deployati su Hetzner
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/`
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. 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