nis2-agile/docs/CONTEXT_LAST_SESSION.md
DevEnv nis2-agile 98fd207096 [DOCS] CONTEXT: review multi-agente + 5 fix (risks/connettori/policy/ingestion/P2)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 11:42:43 +02:00

316 lines
28 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 (gap competitivi P2/P3 + UI) — riepilogo finale
Stato git: origin/main = **`172d927`**, ahead 0. Tutto su Gitea.
### Completato e VERIFICATO E2E in produzione
- **4 feature P1/P2 backend + UI** (sessione mattina): ingestion incidenti, evidence automation, asset import, FAIR/KRI. UI in risks/reports/assets + help/i18n/OpenAPI/scorecard EVIX.
- **P2 Benchmark settoriale anonimizzato**: `GET /dashboard/sectorBenchmark` (k-anonymity ≥3 org) + UI pannello dashboard. **200 OK**.
- **P3 Supply chain self-assessment**: migrazione 027, `SupplyChainController` (sendQuestionnaire JWT + publicQuestionnaire/submitPublicQuestionnaire NO-auth token + questionnaireStatus) + `public/supplier-assessment.html`. E2E: send 201→publicGET 200 (8 domande)→submit 201 (score 61)→re-submit 409→suppliers.risk_score aggiornato.
- **P3 Policy attestation+versioning**: migrazione 028, `PolicyController` (attest/attestations/versions/diff/pendingAttestations + snapshot in approve). E2E: approve→attest(coverage)→bump v2.0→diff(+2/-1)→pending ricompare.
### ⚠️ LEZIONE RICORRENTE (grave, ripetuta più volte oggi)
Le `Edit` falliscono **silenziosamente** quando l'ancora non combacia col file reale, e ho **committato senza ri-verificare via HTTP** → 3 commit P2/P3 erano gusci vuoti (migrazione+HTML ma metodi/route mancanti → 501/404). Corretti con commit successivi (`31b8a45`, `172d927`) DOPO smoke test. **Regola d'oro**: dopo ogni feature, `curl` l'endpoint reale PRIMA del commit; non fidarsi del "Edit success". Inoltre `Edit` richiede Read del file nella sessione corrente.
### 🔌 Connettori per-azienda nella card (RICHIESTA UTENTE, in sospeso — decisione necessaria)
Utente vuole: "le credenziali si configurano nella card per ogni azienda cliente" → card in **companies.html** (consulente), storage segreti scelto **vault-steward**.
**Finding (verificato sull'host)**: vault-steward (`server.js` 2123 righe) e `nis2-vault-proxy` esistono; nis2-app ha `VAULT_APP_TOKEN` scoped `tier1__nis2-app__*` e fetcha al boot (READ). **NON risulta un'API runtime che l'app possa usare per SCRIVERE nuovi segreti**: il populate è operazione ADMIN via `docker exec vault-steward node scripts/vault-repopulate.js` + `register-app` (CLI), non esposta al token applicativo. `VAULT_STEWARD.md` non montato nel devenv.
**DECISO e IMPLEMENTATO** (opzione A — commit `6c8f8e2`): config in DB, secret nel vault via CLI.
- Migrazione `029`: tabella `org_connectors` (organization_id, connector_type ENUM 8 tipi, display_name, enabled, config JSON NON-segreto, vault_key_alias, secret_status, last_status). **NESSUN segreto nel DB.**
- `OrganizationController`: `listConnectors`/`saveConnector`/`deleteConnector` + `connectorOrgGuard` (org_admin/compliance_manager della propria org, o firm che la gestisce, o super_admin). **Difesa secret-strip**: client_secret/api_key/password/private_key/token inviati vengono rimossi prima del save (verificato E2E: NON entrano nel DB). `saveConnector` ritorna `cli_hint` col comando `vault-cli.js migrate <alias> <key> <value>`.
- Route `organizations/{id}/connectors` GET/PUT/DELETE (type nel body).
- UI: pannello "Connettori" nella card di `companies.html` (8 tipi, tenant/client id, toggle attivo, badge stato segreto, modal). Servito 200, JS valido.
- **Vault write-path confermato assente** per l'app: `server.js` (615 righe) espone solo `GET /v1/credentials/*` + tier2 unlock/lock; scrivere segreti è solo CLI admin (`vault-cli.js migrate`). Doc: `/opt/devenv/VAULT_STEWARD.md`.
- E2E in prod: list vuota 200 → save m365 201 (secret strippato) → DB pulito → delete OK.
### Stato git fine sessione: origin/main = `6c8f8e2`, ahead 0. Tutto su Gitea.
### REVIEW MULTI-AGENTE (5 agenti) + FIX — fine sessione 2026-05-30
Lanciati 5 agenti read-only di review (P1, P2, P3, connettori, frontend). Esito: nessuna vuln critica nelle feature, ma trovati problemi reali. **Tutti corretti, testati E2E, pushati.** Stato finale: **origin/main = `dac41f8`, ahead 0**.
- 🔴 `8e3a2c1` **risks.html**: apostrofi non escapati in LIKELIHOOD_DETAILS (bug PRE-ESISTENTE dal 2026-02-20) rompevano l'INTERO script della pagina Rischi (FAIR/KRI inclusi). Fix + verificato LIVE.
- 🔴 `9f4d2a1` **Connettori (SEC)**: (1) `connectorOrgGuard` usava `users.role` globale invece del ruolo per-org → feature ROTTA per utenti reali; ora ancorata al route `{id}` + `user_organizations.role`. (2) secret-strip denylist→**allowlist** ricorsiva. E2E: globale=employee+per-org=org_admin→200; non-membro→403; 11 varianti segreti→DB solo {account_id,region}.
- 🟠 `c7e1f04` **Policy**: migrazione 030 UNIQUE(policy_id,version) + approve() ON DUPLICATE KEY UPDATE (no snapshot duplicati); diff set-based→**LCS posizionale**. E2E ok.
- 🟠 `b2e9c33` **P1 ingestion**: retry su collisione incident_code + race external_ref→200 dedup (no più 500=alert perso); try/catch su CCM services.
- 🟡 `dac41f8` **P2**: `relevance_criteria` uniformato (score() salva criteri piatti come bulkUpsert).
- ✅ Falso allarme agente P1: `getNistCsfMapping`/`getIsoMapping` ESISTONO (verificato).
- Migrazioni totali sessione: 023-030. ⚠️ Le 027/028/029 NON erano state pushate come gusci? No: tutte applicate in prod + codice completo dopo i fix.
---
## 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.
### ✅ Completamento UI + contorni (commit `8a2f4d1`, pushato)
- **UI frontend** delle 4 feature (ora usabili dagli utenti, non solo via API):
- `risks.html`: viste "Quantitativo (FAIR)" (form + istogramma ALE + registro portafoglio) e "KRI" (dashboard semafori + CRUD)
- `reports.html`: tab "Monitoraggio Continuo" (semafori freschezza controlli + copertura)
- `assets.html`: bottone "Importa" CSV/CMDB (parsing client + scoring auto GV.OC-04)
- `api.js`: metodi computeFair/getFairRegister/listKri/createKri/updateKri/importAssets/getControlsMonitoring
- **help.js**: guide FAIR+KRI / import CMDB / monitoraggio continuo. **i18n.js**: chiavi IT/EN.
- **OpenAPI** `/api/services/openapi`: aggiunti incidents-ingest/evidence-ingest/assets-ingest/controls-monitoring + ApiKeyAuth.
- **AuditController::controlsMonitoring** (versione JWT per UI) + route `audit/controlsMonitoring`.
- **EVIX scorecard**: gap P1/P2 marcati chiusi (backend), roadmap P1/P2 con voci ✅ FATTO.
- Verificato in prod: OpenAPI espone i 4 endpoint, pagine 200, import JWT 201, monitoring JWT 200.
### ✅ Gap P2/P3 rimanenti — IMPLEMENTATI (sessione 2026-05-30, pomeriggio)
- **P2 Benchmark settoriale anonimizzato** (commit `a673033`): `GET /dashboard/sectorBenchmark` — confronta score org vs aggregati anonimi del settore (avg/median/p25/p75/percentile/posizione), k-anonymity ≥3 org. UI: pannello dashboard con barra distribuzione. Differenziatore rete multi-tenant. Testato E2E (peers=4, avg/median esatti).
- **P3 Supply chain self-assessment** (commit `8f4e8e9`): migrazione `027` (tabella `supplier_questionnaires`), `SupplyChainController::sendQuestionnaire` (JWT, link token 30gg) + `publicQuestionnaire`/`submitPublicQuestionnaire` (NO auth, token) + `questionnaireStatus`. 8 domande Art.21.2.d pesate → score → `suppliers.risk_score`. Pagina pubblica `public/supplier-assessment.html` (standalone). Pulito route map (rimossi duplicati + `PueT` garbage + metodi inesistenti). Testato E2E (send→public GET→submit→dedup 409→bad token 404).
- **P3 Policy attestation + versioning** (commit `75de1c2`): migrazione `028` (`policy_versions` + `policy_attestations`), `approve()` crea snapshot versione, `attest`/`attestations`(copertura %)/`pendingAttestations`/`versions`/`diff?from&to`. **Version-aware**: bump versione → richiede ri-attestazione (verificato E2E: diff added/removed corretto, pending ricompare dopo v2.0).
### ⚠️ Trovato file PRE-CORROTTO (non toccato)
`public/supply-chain.html` è **corrotto in HEAD da prima** (committato 2025-09-29): righe garbage `the ▐ }`, `const file = list.clea; // BUG`, funzioni duplicate. NON è di questa sessione. La UI admin per inviare questionari fornitori va aggiunta **dopo** aver riparato questa pagina (task separato). Il backend + portale pubblico P3 supply chain funzionano comunque via API.
### ❌ Restano (prossimi passi reali)
- **Riparare `public/supply-chain.html`** (pre-corrotto) + aggiungere UI admin "Invia questionario" / stato attestation policy / lista versioni+diff.
- Connettori **per-vendor live** (OAuth M365/Google/AWS/Azure/IdP/EDR): le 8 card sono "In roadmap"; servono client + credenziali/app-registration lato cliente.
- **Auto-discovery attiva** asset (agent/scan) — l'import bulk e gia disponibile.
- Gap **P2 Reporting/benchmark settoriale** e **P3 Supply chain/policy** non ancora affrontati.
- NB workflow: OPcache `validate_timestamps=Off` → dopo ogni modifica PHP serve `docker exec nis2-app sh -c 'kill -USR2 1'`. Le modifiche a HTML/JS sono servite da nginx senza reload.
---
## 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