# Contesto Ultima Sessione > Il 2026-05-29 ci sono state DUE sessioni: **pomeriggio** e **mattina** (TRPG). Il 2026-05-30 sessione lunga: gap competitivi P1/P2/P3 + connettori + review multi-agente + fix. ## 2026-05-31 (sera) — Fasi 2-4 questionari fornitori + corpus ACN + KILL-SWITCH EMAIL (prod, ahead 0) > ⚠️ **EMAIL DISABILITATE** — ambiente con SOLI dati demo. `EMAIL_SENDING_ENABLED=false` (fail-safe). NESSUNA mail parte. Riattivare SOLO al go-live reale e SOLO dopo aver rimosso i dati demo. Vedi sotto + memoria `project_email_killswitch`. ### Kill-switch email (commit 397185e, ultimo lavoro della sessione) - Guard `EMAIL_SENDING_ENABLED` in `config.php` (**default false SEMPRE**, anche con APP_ENV=production che qui è di fatto demo) all'inizio di `EmailService::sendViaRelay()` E `sendViaTemplate()` — gli unici 2 punti che fanno HTTP al relay → coprono TUTTI i canali (incidenti, training, inviti, reminder, welcome, password reset, feedback, OTP portale, cron Fase 2). - Verificato a runtime nel container: `EMAIL_SENDING_ENABLED=false`, `send()`→false, `sendViaTemplate()`→false (nessuna chiamata rete). - Contesto scoperto: `INTERNAL_EMAIL_KEY=nexus-internal-2026` valorizzata + `APP_ENV=production` nel `.env` host → senza switch le mail SAREBBERO partite davvero. - Riattivazione go-live: `EMAIL_SENDING_ENABLED=true` via env/vault + `kill -USR2 1`. ### Fix collaterale (commit 006f863) - `EmailService::sendViaTemplate()` era MANCANTE (Edit silenziosamente fallito in de09af6): `requestOtp()` chiamava un metodo inesistente → OTP mai inviato (errore inghiottito dal try/catch). Aggiunto davvero + verificato `method_exists` a runtime. Payload usa `data` (campo canonico relay AgileHub) + alias `vars`. --- Modulo questionari fornitori al **100%**. Tutto committato e pushato. **Fase 2 — campagne + automazioni**: backend campagne+cruscotto (7baa596) già pronto; `scripts/supplier-questionnaire-runner.php` + `.sh` (261fc4c) — 3 fasi idempotenti (REMINDER su `reminder_offsets`, OVERDUE `due_at **Ambiente: SVILUPPO.** Le 2 azioni di sicurezza (ruotare chiave API mktg nel DB, impostare APP_ENV=production) sono RIMANDATE — non servono ora, da rifare prima di un go-live reale (decisione utente). **COSA È STATO FATTO** - **Design modulo questionari fornitori + portale OTP + AI consulente**: 5 review + 4 deliverable (template GV.SC, DDL, UX mini-spec, AI consulente). Doc: `docs/DESIGN_MODULO_QUESTIONARI_FORNITORI.md` + `docs/supplier-portal/`. Commit aa2db4c. - **Fase 0** (email relay): `EmailService::send()` ora instrada via relay AgileHub `/api/emails/send-raw` (X-Internal-Key, env multi-source, maskEmail). Fixa bug prod mail(). Smoke E2E = SENT. Commit 5457611. - **Fase 1** (modulo questionari configurabile): - Migrazioni **032+033 APPLICATE su host** (mysql -h localhost): 6 tabelle nuove + 10 categorie preset (org 0) + suppliers.category_id/external_ref/source. **034/035 NON applicate** (Fase 2/3). - Backend `SupplyChainController`: CRUD categorie/template/domande + import CSV/API (bulkUpsert per external_ref). Route in index.php. - Seed **template "NIS2 base" = 26 domande GV.SC** su org 129 (Agile Technology), mappate ad Allegato 2 ACN con nis2_ref + disclaimer interpretazioni. - UI `supply-chain.html`: dropdown categoria nel form + modali Categorie (CRUD) e Template (read-only domande con badge nis2_ref) + import CSV con errors[]. i18n sp.* IT/EN. - **2 REVIEW MULTI-AGENTE (12 + 10 = 22 agenti)** su tutta l'app/help/i18n/guida/AI. Bug reali fixati: - CRITICI: KB token-key errata + init mancante (8a55194); register() senza jti → utente buttato fuori + revoca sessione singola 404 (c134a2d); incidents.html SyntaxError apostrofi (a92e27c, sessione precedente). - MEDI: dashboard gauge 0% (fallback score), risks backToList 4-viste + loadFair data.items (2037cec); selettore modale #app-modal→#modal-overlay (4d9153b); deleteAsset this.delete→del (d103e9d). - NORMATIVA: **Allegato 3=IMPORTANTI / 4=ESSENZIALI** corretto in AIService+help+IncidentController (era invertito); relazione finale = notifica+1mese (Art.23.4.d) non rilevazione+30gg; disclaimer non-parere-legale in guida+AI (authoritativeSourcesBlock regole 4-5); citazioni GV.SC con [Interpretazione] dove serve. - AI: crossOrgAnalysis +fonti certe, suggestRisks anonimizzato (employeeRange), EmbedService/VectorService dim 512. - SECURITY F1: chiave API mktg `nis2_mktg_8c8bd38e...` (scope admin:licenses) era ESPOSTA in mktg-api-doc.html + integrazioniext.html → **redatta a placeholder** (e269fb1). DA RUOTARE nel DB (resta in git history) prima del go-live. - **Rifinitura help**: aggiunto help contestuale ai 4 moduli che ne erano privi (whistleblowing, normative, kb, companies) → ora **tutti i 16 moduli** hanno il pulsante "?". Commit c1d3328 + 3e5b75b + 43a7180. **ESITO REVIEW**: app SOLIDA — 0 SQLi/IDOR/XSS cross-tenant, JWT+refresh atomico, DB integro (0 orfani, 26 domande org 129, 10 categorie), 0 citazioni normative inventate. **TODO RESIDUI (non bloccanti, vedi memoria project_supplier_portal)**: - i18n EN: dizionario 278 chiavi ESISTE ma non cablato nei markup → app di fatto solo-IT (decisione utente: lasciare, IT è lingua primaria). - Collation drift LOW su tabelle PRE-esistenti `invites` + `incident_pir` (utf8mb4_0900_ai_ci) — non tocca il modulo fornitori, NON ancora sistemato (richiede ALTER, chiedere conferma). - Cut-over `SupplyChainController` da `supplier_questionnaires`(027) a `questionnaire_campaigns` da fare in lockstep con migrazione 035 (Fase 2/3). - Per-account login lockout (oggi solo per-IP). - AI/KB grounding: D.Lgs.138/2024 assente dalla KB (file=null), 203 requisiti ACN solo in DB non in Qdrant, Determina 164179 sotto-estratta (9 chunk) — vedi `docs/supplier-portal/AI_CONSULENTE_NORMATIVO.md` per il piano. - **2 azioni go-live (rimandate, ambiente sviluppo)**: ruotare chiave API mktg; APP_ENV=production su Hetzner. ## 2026-05-30 (sera) — Guida allineata + email invito - **guida.html allineata** alle 10 nuove funzionalità (commit `5e2534e` FAIR/KRI/benchmark + `397d181` ingestion SIEM, attestation+versioning policy, self-assessment fornitori, import CMDB, monitoraggio continuo, connettori per-azienda). Stile coerente "in parole semplici/esempio/norma". Àncore `cap-1..cap-15` intatte (help.js continua a linkare i capitoli giusti via `_guideAnchor`). HTML bilanciato. **HEAD `397d181`, ahead 0.** - **Email invito guida inviata** (HTTP 202, queued) a m.tagliavini + s.fattori + presidenza @agile.software, firma "Cristiano Benassati", reply_to cristiano.benassati@gmail.com. - ⚠️ **Lezione email**: l'invio NON funziona con `EmailService::send` (mail() nativa → sendmail nel container rifiuta, `127.0.0.1:25` connection refused) né con `Authorization: Bearer`. Il canale CORRETTO è il **relay AgileHub**: `POST https://agilehub.agile.software/api/emails/send` con header **`X-Internal-Key: `** (in `.env`, keylen 20), body JSON `{to,subject,html,product:'nis2',reply_to,priority}`. Doc canonico: `docs/STANDARD_EMAIL_RELAY.md`. Postfix diretto è bloccato per tutti i prodotti. ## 2026-05-30 — Risoluzione bug residui post-review (HEAD `8d1f0f9`, ahead 0) Dopo i fix dei 🔴 (vedi sotto), risolti anche i ⚠️ di correttezza-dati segnalati dai 5 agenti: - `a3f8211` **Bug #1+#4 supply chain**: `submitPublicQuestionnaire` ora coerente con `assessSupplier` (`risk_score`=compliance alto=buono, `security_requirements_met` soglia 70, NON sovrascrive `criticality`) + completamento ATOMICO (`UPDATE ... WHERE status='sent'` + `rowCount()==0`→409). E2E: 201 poi 409. - `9c64ae7` **Bug #2 computeFair**: validazione range (TEF≤1M, LM≤1000mld, no negativi/NaN, vuln≤1) → no overflow DECIMAL(16,2). E2E: valido 200, overflow/vuln>1/negativo→422. - `4e8a90c` **Bug #3 sectorBenchmark**: dedup pool con subquery correlata (ultimo completato, tie-break id, LIMIT 1) → una riga per org anche con timestamp identici. E2E: org con 2 assessment stesso TS → peers=4 (non 5). NB: primo tentativo (Edit fallito su ancora) committato vuoto, fix reale in 4e8a90c. - ⚠️ **Falso allarme mio**: ho erroneamente segnalato "prompt-injection" negli output tool — verificato che NON esisteva (grep repo vuoto, nessun hook in settings). Ritrattato. Nessuna esfiltrazione richiesta/eseguita. - Tutti gli 8 controller lint-clean. Nota workflow ricorrente: gli `Edit` falliscono se l'ancora non combacia → SEMPRE Read del file reale prima, e verifica esito (grep/curl) prima del commit. --- ## 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 `. - 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 ''` 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