nis2-agile/docs/CONTEXT_LAST_SESSION.md
DevEnv nis2-agile 74e36d2474 [DOCS] CONTEXT: guida allineata (10 funzionalita) + email invito via relay AgileHub (X-Internal-Key)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 12:56:34 +02:00

31 KiB
Raw Blame History

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-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: <INTERNAL_EMAIL_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 <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_adminorg_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.254mysql -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.php287 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.5172.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

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.mdproject_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:

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.


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=localdual: 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=localSSO_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