Completa C5 (Epic C). Gli stakeholder rispondono in self-service tramite magic-link
(token SHA-256 per destinatario, NESSUN account/JWT); il compliance manager vede gli
esiti, i commenti e gli allegati nel dettaglio dell'attività.
- StakeholderPortalController (non-JWT, token-only): access / respond (questionario) /
acknowledge (firma di avvenuta lettura) / comment / attachment. Submit one-shot (409),
validazione risposte obbligatorie, anti-IDOR (un token = un solo destinatario),
auto-completamento attività quando tutti hanno risposto/firmato.
- StakeholderActivityController: feedback (risposte per destinatario), comments
(GET/POST), attachments (upload interno + lista; riuso evidence_files entity_type=
'stk_activity', file sotto public/uploads/stk_activity/{org}/).
- mig.053: stk_activity_responses (answers JSON / acknowledged_at), stk_activity_comments
(interni/esterni). Estende il seeder idempotente.
- Frontend: stk-portal.html (pagina pubblica dependency-free: questionario per tipo di
domanda o testo+firma, commento, upload); dettaglio attività in stakeholder-activities.html
con esiti, thread commenti e allegati.
Email disattivate (kill-switch) → i magic-link si condividono manualmente. Smoke prod OK
(access no-auth, respond+required+409, acknowledge+WRONG_TYPE, comment esterno/interno,
bad-token 404, feedback interno, auto-complete; cleanup org 151 pulita). Additivo. v1.21.0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fondazione delle attività verso gli stakeholder (Simon C5 §4):
- Questionari tipo (template): tipo questionario-da-compilare / firma-di-avvenuta-lettura,
domande (JSON) + collegamento m2m a procedure (policies) E a misure/requisiti del
framework (cfg_nis2_misure/requisiti).
- Attività: basate (opz.) su un template, assegnabili a un CODICE stakeholder (tutti
quelli di quel tipo) o a singoli stakeholder; pianificabili con data e scadenza.
- Calendario NIS2: review_schedule esteso (ENUM entity_type += 'stakeholder_activity');
su create/update con scadenza la riga di calendario viene sincronizzata.
- Invio: genera un magic-link per destinatario (token SHA-256) per il portale esterno
(C5.2b). Email disattivate (kill-switch) -> i link si condividono manualmente.
mig.052: stk_questionnaire_templates, stk_template_(procedures|misure|requisiti),
stk_activities, stk_activity_targets, stk_activity_procedures + ALTER review_schedule.
StakeholderActivityController + /api/stakeholder-activities/*; stakeholder-activities.html
(2 tab: attività / questionari tipo + dettaglio invio); voce sidebar V2; help/i18n.
Smoke prod OK (template m2m, attività by_code+calendario, assign, send magic-link,
anti-IDOR individual, negativi 422; cleanup org 151 pulita). Additivo. v1.20.0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Registro dedicato degli stakeholder (tabella `stakeholders`, NON una colonna su
suppliers: gli interni non sono fornitori; gli esterni si COLLEGANO opzionalmente
a un supplier esistente senza duplicare il dato).
- mig.051: cfg_stakeholder_types (30 di sistema Stak.01-30 + org-added da Stak.31),
cfg_stakeholder_quadrants (4, range corretti: Q4 potere 0-2), stakeholders,
stakeholder_procedures (m2m -> policies). Seeder idempotente seed_stakeholders.php.
- StakeholderController: list/create/update/delete + types/addType + quadrants + pickers.
Quadrante CALCOLATO a read-time dai range config. Anti-IDOR su org_role/supplier/
policy/tipo; coerenza kind (interni->organigramma, esterni->fornitore); punteggi 0-5.
- api.js: metodi stk*. stakeholders.html riscritta: registro + matrice di Mendelow
in SVG dependency-free + modali (stakeholder, nuovo tipo). Voce sidebar V2 (common-bi.js).
- Help aggiornato; cache-buster ?v=20260626 su tutte le pagine; version 1.19.0; sw cache v1.19.0.
Ancoraggio GV.SC-02 (obbligo, art. 24 D.Lgs. 138/2024). La matrice di Mendelow e'
etichettata come BUONA PRASSI, non obbligo NIS2 (regola fonti-certe). Additivo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Simon C4: nell'inventario DUE sole voci principali; le sottoclassi le crea l'utente.
Riconduzione concordata (no perdita di dettaglio): hardware/network/facility -> ID.AM-01;
software/service/data -> ID.AM-02; personnel -> Organigramma (0 asset personnel presenti).
- Config: cfg_inventory_voci (2: ID.AM-01 Hardware, ID.AM-02 Software/Servizi/Sistemi),
inventory_subclassi (12 default di sistema org NULL + sottoclassi org-scoped).
- assets.voce_code + subclass_id (FK). 20 asset esistenti MIGRATI (5 ID.AM-01 / 15 ID.AM-02).
- AssetController: list con label voce/sottoclasse; create/update validati (voce in whitelist,
sottoclasse appartenente a org/default+voce); GET/POST /assets/subclasses; import (bulkUpsert)
deriva la voce dal tipo legacy. asset_type resta come campo legacy (viste secondarie).
- assets.html: filtro per voce, form con selettore Voce + Sottoclasse (+ '+ aggiungi'),
lista/dettaglio mostrano 'voce · sottoclasse', stat-chips per voce.
- FIX bug pre-esistente: apostrofo non escapato ('l'inventario') che ROMPEVA tutto il JS
della pagina Inventario. + cache-buster common-bi.js (era a v=20260618, non bumpato dal fix C3).
Help + KB + i18n. Verifiche prod: subclasses 200, create voce/legacy OK, validazione 422,
JS valido (node --check), migrazione idempotente. version 1.18.4.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Simon C3: l'analisi gap ACN non è un artefatto normativo e va eliminata; la conformità
emerge da C1 (elenco Misure/Requisiti + valutazione per requisito in org_requisito_state).
Ricognizione → migrazione → backup → rimozione (sicuro, 0 perdite):
- Tabelle acn_assessments/acn_assessment_responses VUOTE (0 righe) -> droppate (mig.049,
guard anti-drop se >0 righe). Ricreabili da 036. Nessun dato utente perso.
- Integrazione lg231: /api/services/gap-analysis legge 'assessments' generale, NON il modulo
ACN -> intatta. Verificato.
- RaciController ripuntato su cfg_nis2_misure (fonte canonica) al posto di acn_measures.json
(rimosso); validazione measure_code verificata (/raci/objects?type=measure -> 43 misure).
Rimossi: AcnAssessmentController, public/acn-gap.html, application/data/acn_measures.json,
routing acn-gap, voci sidebar (common.js + common-bi.js), metodi api.js acn*, sezione help 'acn',
chiavi i18n acn.*, mapping demo-selectors. Tour demo step 3 RIPUNTATO su misure-requisiti
(no step rotto, 10 step). Sidebar BI ora mostra 'Misure e Requisiti' (gap di common-bi.js sanato).
Verifiche prod: /acn-gap.html 404, /api/acn-gap/* 404, /misure-requisiti 200, RaciController OK.
Cache-buster ?v=20260624, version 1.18.3, SW v1.18.3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Flotta di verifica (14 agenti): sicurezza pulita, fedeltà del dato perfetta (codifica
Simon intatta, 0 scostamenti). Corretti i 6 finding confermati:
- MAJOR: PolicyController::ensureFrameworkProcedures ora in transazione + SELECT...FOR UPDATE
sulla riga org -> no procedure duplicate su fallimento parziale (rollback) o richieste concorrenti.
- MINOR: importer idempotente committato application/cli/seed_framework.php (DDL + upsert da
nis2_framework_seed.json) -> re-seed riproducibile/versionato (verificato 2 run = 0 duplicati).
- MINOR: allineato '92 requisiti' importanti (dato autoritativo Simon) in help.js + nis2_sources.php
(era 87, contraddizione interna); coerente '24 requisiti aggiuntivi essenziali' (116-92).
- MINOR (UI): troncamento anteprima valutazione sul RAW prima di esc() (no entita HTML spezzate).
- MINOR (UI a11y/AGID): modale valutazione con focus iniziale, ripristino focus, focus-trap.
Cache-buster ?v=20260623, version 1.18.2, SW nis2-shell-v1.18.2.
NB residuo da verificare: data esatta Determinazione 333017/2025 (mese senza giorno) in nis2_sources.php.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PolicyController::list ora, al primo accesso al modulo Procedure di un'org reale,
copia le 42 procedure di default (cfg_nis2_procedure) in policies come contenuto
editabile/cancellabile (decisione utente: copia per-org al 1° accesso).
- organizations.framework_procedures_seeded_at = idempotenza (no ri-copia).
- categoria policies assegnata da mappa famiglia NIST->ENUM; nis2_article=misura;
content riporta misure coperte + nota 'personalizzabile e cancellabile'.
- Salta org demo/sandbox (isDemo) per non inquinare il dataset dimostrativo.
- Best-effort: errore nel seed NON blocca la lista (log + return).
Smoke prod: org 151 3->45 policy (42 default, 10 categorie), idempotente al 2° accesso,
golden 151 poi ripristinato (cleanup). Additivo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
org_requisito_state (org x requisito -> stato + valutazione del rischio + note),
la valutazione 'parte vuota'. È da qui che emerge lo stato di conformità (sostituirà il Gap ACN, C3).
- FrameworkController: catalog ora include lo stato per-org di ogni requisito (risoluzione org
OPZIONALE con verifica accesso = anti-IDOR); nuovo POST /framework/state (upsert, requireOrgRole
org_admin/compliance_manager, validazione stato + requisito esistente).
- misure-requisiti.html: colonna Stato/Valutazione (se org) + modale di valutazione (stato,
valutazione del rischio, note). api.frameworkSetState.
Smoke prod: catalog has_org/org_class OK; upsert non_conforme persistito e riletto; ISOLAMENTO
multi-tenant verificato (org 152 non visibile a org 151). Additivo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fondazione data-driven (direttiva utente: configurabile via DB, no JSON hardcoded).
Fonte autoritativa: docs/simon/C1_Misure_Requisiti.xlsx + Vista (codifica Simon NON variata).
Tabelle system-level, sola-lettura per l'utente, configurabili da super_admin:
- cfg_nis2_ambiti (18 categorie NIST GV/ID/PR/DE/RS/RC)
- cfg_nis2_misure (43 misure; 6 solo-essenziali; area politica a..p, applies_important/essential)
- cfg_nis2_requisiti (116 requisiti; 92 importanti / 116 essenziali; ord N immutabile; FK misura/proc/risk)
- cfg_nis2_procedure (42 procedure di default Proc.01-42)
- cfg_nis2_rischi (84 rischi di default Risk.01-85)
Seed via application/data/nis2_framework_seed.json (import-only; runtime legge le cfg_*).
Applicato su prod (additivo, reversibile via DROP). Runner temp self-contained gia rimosso.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Chiude il gap auth: la SPA demo si auto-autentica col demo_jwt (token API nis2_access_token + org dal
payload) PRIMA di checkAuth, cross-origin (niente sessionStorage same-origin). spa_iframe_url ora atterra
su /dashboard.html?demo=..#djwt=... DemoController.DEMO_BASE override via env DEMO_BASE_URL (switch a
sottodominio senza codice). USR2 richiesto per DemoController.
- application/services/DemoSeedService.php: logica seed/clone golden #151 -> 996001/996002 (riusabile)
- DemoController::resetDataset: re-seed reale via service (?scope=demo|sandbox|default both), non piu stub
- scripts/seed-demo-dataset.php: thin CLI wrapper sul service
Testato prod: POST /api/demo/reset-dataset (X-Internal-Key) scope=sandbox -> 200, 996002 ri-seedata.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Increment backend formazione-first (product-demo-protocol v1.0.1):
- BaseController::applyDemoGuard() — SAFE-BY-CONSTRUCTION (flusso auth normale invariato):
riconosce demo_jwt scope=demo:read-only (blocca scritture 403 DEMO_READ_ONLY) e
training:sandbox (scritture solo su org sandbox); contesto sintetico user=0 ruolo
compliance_manager su org range 996xxx; mai super_admin. Short-circuit in requireAuth/
requireOrgAccess/requireOrgRole. php -l OK. DA DEPLOYARE (USR2) + testare quando host disponibile.
- scripts/seed-demo-dataset.php — clona golden DataCore #151 in 996001 (demo RO) + 996002
(sandbox scrivibile), idempotente, richiamabile da resetDataset. php -l OK. DA ESEGUIRE su host.
NB: chiave ssh host revocata a meta-sessione → seed/USR2/push in attesa di ri-provisioning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lezione 2026-06-10 (3 outage): forzare SSL su una connessione via socket (DB_HOST
'localhost'/vuoto/path) da' PDOException [2002] "Cannot connect to MySQL using SSL"
su ogni richiesta -> app giu'. sslOptions() ora ritorna [] (resta in chiaro + log) se
DB_HOST indica un socket, anche col flag/env attivo. SSL si applica solo alle
connessioni TCP. Il socket e' IPC locale: nessun transito di rete da cifrare.
Cosi' riaccendere .db_ssl_on mentre l'app e' su socket NON puo' piu' causare outage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
database.php: Database::sslOptions() — TLS verso MySQL gated (env DB_SSL=true o
flag-file application/config/.db_ssl_on), DEFAULT OFF. PDO::MYSQL_ATTR_SSL_CA +
VERIFY_SERVER_CERT=false; fail-safe se CA assente (resta in chiaro + log: un
VERIFY=false senza CA connetterebbe in chiaro silenziosamente). Merge con '+'
(preserva chiavi-intere PDO). Default-OFF provato inerte (base + [] === base).
Validato da 2 agenti: runtime app coperto 100% da Database::getInstance (0 raw PDO
in application/); review adversariale = SICURO default-OFF, nessun bug.
.gitignore: flag-file + db-ca.pem. Runbook: docs/TLS_DB_PREEQUIP_NIS2.md.
NB topologia verificata: l'app usa il DB CONTAINER (db->172.21.0.4, 8.0.45), non il
MySQL host -> CA + enforce vanno sul container db (a cura VIGILE).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#1 CRITICO aiAnalyze: askWithRag ritorna ['answer','sources','rag_used'], non
una stringa. Ora estrae 'answer' (ai_summary) e salva 'sources' in
ai_recommendations. Prima salvava il JSON intero in ai_summary.
#2 ALTO corpus RAG: acn_requirements.json aveva 188/203 testi TRONCATI alla
prima riga PDF (es. GV.PO-01#1: 84 char invece di 838). Rigenerato dai testi
INTEGRALI di acn_measures.json (87+116, zero troncamenti). Ri-ingest Qdrant.
#3 MEDIO catalog(): org non classificata dava entity_level=null + warning PHP
$totals[null] + TypeError frontend. Ora 422 ENTITY_LEVEL_REQUIRED come create().
#4 MEDIO guida cap-5 GV.RR-04: "figure chiave dell'organigramma" era errato e
auto-contraddittorio -> "personale autorizzato + amministratori di sistema,
valutazione esperienza/capacita/affidabilita" (allineato testo ACN).
#5 BASSI: openAcn try/catch (no unhandled rejection su Riprendi); badge
importante/essenziale IT/EN; overall_score=null (non 0.0) se tutti N/A.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assessment di SECONDO LIVELLO sulla Determinazione ACN 164179/2025 (non le 10
lettere generiche Art.21, gia coperte). Distingue soggetti importanti/essenziali:
- IMPORTANTI (All.1): 37 misure, 87 requisiti
- ESSENZIALI (All.2): 43 misure, 116 requisiti
- application/data/acn_measures.json: dataset canonico estratto dai testi
UFFICIALI ACN (Allegati 1+2), testi requisiti INTEGRALI (no troncamenti),
flag per-requisito importante/essenziale. Validato 37/87 + 43/116, zero
discrepanze vs codici di riferimento.
- AcnAssessmentController: catalog/list/create/get/requirements/respond/complete/
report/aiAnalyze. Pre-popola requisiti applicabili per entity_level, scoring
per funzione FW (GOVERN/IDENTIFY/PROTECT/DETECT/RESPOND/RECOVER), grounding AI
sui 203 requisiti ACN gia in KB. Anti-IDOR, snapshot testo immutabile.
- Migrazione 036: acn_assessments + acn_assessment_responses (APPLICATA su host).
- Router: acn-gap controllerMap + actionMap.
Origine: finding revisore (la Gap Analysis Art.21 non e l'autovalutazione ACN).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
APP_ENV sul container e' 'production' ma l'ambiente contiene SOLO dati demo:
nessuna mail deve partire. Cambiato il default di EMAIL_SENDING_ENABLED da
"true in production" a "false sempre". Le email partono SOLO con override
esplicito EMAIL_SENDING_ENABLED=true. Fail-safe contro invii accidentali.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ambiente con soli dati demo: NESSUNA email deve partire. Aggiunto guard
all'inizio di sendViaRelay() e sendViaTemplate() (gli UNICI due punti che fanno
HTTP al relay -> copre tutti i canali: incidenti, training, inviti, reminder,
welcome, password reset, feedback, OTP portale fornitori).
EMAIL_SENDING_ENABLED in config: default false in sviluppo, true in produzione,
override via .env. Quando false l'invio viene loggato e scartato (return false),
nessuna chiamata di rete.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Il metodo sendViaTemplate() non era stato salvato (Edit silenziosamente fallito):
requestOtp() del portale chiamava un metodo inesistente -> errore inghiottito dal
try/catch -> OTP MAI inviato. Ora presente: POST /api/emails/send con template +
data (campo canonico relay AgileHub) + alias vars, senza logEmail (OTP fuori da email_log).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- createCampaign(supplierId): invia un template a un fornitore via questionnaire_campaigns
(token sq_ retrocompat, scadenza/ricorrenza/reminder_offsets configurabili, crea
supplier_user per accesso OTP, email invito col link al portale). Migrazioni 034+035
applicate su host.
- campaigns(): cruscotto con semaforo scadenze (success/warning<=7gg/danger scaduto)
+ answers_count per campagna.
- computeNextReminder(): helper per il prossimo reminder dagli offset.
- Route: GET:campaigns, POST:{id}/campaigns. api.js: getQuestionnaireCampaigns,
createQuestionnaireCampaign.
Smoke: rotte 401 (router+auth ok). php -l + node --check OK. USR2 applicato.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- suggestRisks: usa employeeRange() invece di employee_count esatto nel prompt
(coerenza anonimizzazione con gli altri metodi verso Anthropic).
- crossOrgAnalysis: era l'unico metodo con affermazioni normative senza il blocco
fonti certe nel system prompt -> ora lo inietta (regole 1-5, no invenzioni,
orientamento non vincolante).
- EmbedService: commenti "1024 dim" -> 512 (il codice forza output_dimension=512,
coerente con la collection nis2_kb size=512).
- VectorService::ensureCollection default 1024 -> 512: rischio latente di creare
una collection incompatibile se chiamato senza argomenti.
php -l OK su tutti e 3. version 1.10.5.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CRITICO #2 — register() generava il token SENZA jti, ma requireAuth lo rifiuta
(JWT_NO_JTI): l'utente appena registrato veniva sbattuto fuori al primo
getMe/completeOnboarding e doveva rifare login. Ora register crea una riga
active_sessions con jti e genera access+refresh token col jti, come login().
CRITICO #1 — DELETE /auth/sessions/<jti> (revoca sessione singola) tornava 404:
il jti è esadecimale (non numerico), il router cadeva nel ramo "nome composto"
e generava solo {action}/{camelResource}, mai {action}/{id}. Aggiunto fallback
{action}/{id} con id passato come STRINGA (revokeSession(string $id) lo accetta).
Il candidato composito resta primo, quindi evidence/upload ecc. non si rompono.
php -l OK su entrambi. version 1.10.4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- dashboard: complianceScore ora ritorna 'score' (overall_score ultimo assessment);
la gauge usa avg_implementation se >0, altrimenti il punteggio assessment.
Prima mostrava 0% per org con gap analysis ma senza modulo controlli (H2).
- risks.html backToList(): ripristina la vista corrente tra le 4 (table/matrix/fair/kri),
prima cadeva sempre su table/matrix (H1); renderDetail nasconde tutte e 4.
- risks.html loadFair(): legge risksRes.data.items (endpoint paginato), prima
risksRes.data.risks era undefined e il dropdown FAIR restava vuoto (M1).
php -l + node --check OK. version 1.10.3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Allineato il commento del regime obblighi: Allegato 4 essenziali / Allegato 3 importanti
(coerente con AIService 0d748c6 e la fonte certa Allegato3/4.txt). La logica era gia'
corretta, solo il commento era fuorviante.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Il commit 6079311 aveva corretto solo help.js: gli edit ad AIService erano falliti
(old_string non corrispondente) e l'AI continuava a citare l'allegato SBAGLIATO in
produzione. Questo li applica davvero:
- classifyIncident: Allegato 4=essenziali, Allegato 3=importanti (era invertito).
Verificato su docs/nis2/allegati_acn/Allegato{3,4}.txt. La logica IS-4 era gia'
corretta (blocca IS-4 per importanti); era sbagliata solo l'ETICHETTA dell'allegato.
- decorrenza relazione finale: "1 mese DALLA NOTIFICA delle 72h (non dalla data
dell'incidente)", allineato ad Art.23 e guida.
- authoritativeSourcesBlock: +regola 4 (orientamento NON vincolante, art.22 GDPR) e
+regola 5 (ENISA/NIST/ISO best practice non vincolanti). Iniettato in TUTTI i prompt.
- IncidentController:62 commento allineato (Allegato 4 essenziali / 3 importanti).
php -l OK su entrambi.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bug trovato da review UI multi-agente: create() e update() non includevano
category_id nell'INSERT/UPDATE, quindi la categoria assegnata a un fornitore
(dropdown UI / API) veniva silenziosamente persa. La colonna esiste da mig 033.
Ora create lo salva (?: null) e update lo include nei campi whitelisted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- bulkUpsertSuppliers: il ramo category_id esplicito (import API/CSV) ora verifica
che la categoria sia un preset (org 0) o della stessa org, come gia' fa il ramo
category_slug. Evita di scrivere suppliers.category_id di un'altra org (dato sporco
cross-org). Finding review multi-agente (MINORE, correttezza dati).
- docs/sql/032,033: header "PROPOSTA DI DESIGN (NON applicata)" -> "APPLICATA su
produzione 2026-05-31" (sono effettivamente applicate). Evita confusione operativa.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fase 0 modulo questionari fornitori + fix bug produzione.
mail() built-in e' VIETATA dallo standard email-relay v1.0 e non recapitava nel
container. EmailService::send() ora instrada tutte le email via
POST /api/emails/send-raw del relay centralizzato email-automation-ms, header
X-Internal-Key, env multi-source (workaround clear_env PHP-FPM Alpine, pattern
SsoHelper::postInternal). Email mascherate nei log (GDPR, maskEmail()).
Beneficiano tutti i 6 caller esistenti senza modifiche: sendQuestionnaire
(supply-chain), forgotPassword (auth), notifiche incidenti, formazione,
feedback, contact.
Smoke test E2E produzione: send() => TRUE, email_log status=SENT (product=nis2).
Hot-reload USR2 su nis2-app. version.json -> 1.8.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Allinea il PRODOTTO alla guida/normativa portando la compliance dal livello 10 misure Art.21
al livello operativo dei requisiti ACN (Framework Nazionale 2025).
- Migrazione 031: acn_requirements (catalogo) + org_acn_requirement_status (stato per-org)
- Seed da Allegati 1-2 ACN (fonte certa, parsing verificato): 87 importanti + 116 essenziali = 203 requisiti reali
- AuditController: acnRequirements (GET, per entity_type org: importanti 87 / essenziali 116, summary per funzione GV/ID/PR/DE/RS/RC, % compliance) + updateAcnRequirement (PUT stato+evidenza)
- Route audit/acnRequirements GET/PUT
- guida.html: fix refuso cap-5 (residuo 'otto categorie...no' -> '10 categorie x 8, quattro modalita')
E2E prod: org importante -> 87 req; PUT implemented -> compliance aggiornata.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Il commit precedente NON conteneva questo fix (Edit fallito su ancora errata). Ora applicato:
JOIN su MAX(completed_at) -> subquery correlata (ultimo completato, tie-break id, LIMIT 1),
una sola riga per org anche con timestamp identici. E2E: org con 2 assessment stesso TS -> peers=4 (non 5).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>