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>
- ingestIncident: insert in loop (max 5) -> rigenera incident_code su collisione UNIQUE
(sotto carico SIEM il random a 6 cifre poteva collidere -> 500 = alert perso). Inoltre la
race su external_ref (due alert simultanei) ora ritorna 200 dedup invece di 500.
- controlsMonitoring (services): UPDATE auto-stale avvolto in try/catch come la gemella in
AuditController (degrada con grazia se control_evidence_auto manca).
Verificato E2E: ingest 201, dedup 200.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Migrazione 030: UNIQUE uq_policy_version su policy_versions (de-dup prima, idempotente).
approve() ora usa INSERT ... ON DUPLICATE KEY UPDATE -> riapprovare la stessa versione
aggiorna lo snapshot invece di duplicarlo. Verificato E2E: 2x approve v1.0 -> 1 sola riga.
- diff(): sostituito il confronto set-based (falsi negativi su righe duplicate/riordino) con
un vero diff LCS line-by-line con posizioni. Verificato E2E: bump v1->v2 -> added 2, removed 1 corretti.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Due vulnerabilità trovate dalla review indipendente:
1. connectorOrgGuard usava users.role (GLOBALE) invece del ruolo per-org -> la feature
era ROTTA per gli utenti reali (org_admin reale ha users.role='employee' -> 403 sulla
propria org). Ora ancora l'autorizzazione al parametro di ROUTE {id} e legge
user_organizations.role. Verificato E2E: globale=employee + per-org=org_admin -> 200;
non-membro su altra org -> 403 (no IDOR via header X-Organization-Id).
2. secret-strip era una denylist case-sensitive/non-ricorsiva aggirabile (Client_Secret,
apiKey, connection_string, segreti annidati). Sostituita con ALLOWLIST ricorsiva
(sanitizeConnectorConfig): solo campi non sensibili noti, valori forzati a stringa+troncati.
Verificato E2E: input con 11 varianti di segreti -> DB contiene solo {account_id, region}.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Verificato E2E in prod: list 200 (8 tipi), save m365 201, secret 'client_secret' STRIPPATO (assente da config DB), delete 200, openConnectors servito in companies.html.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
I commit 56ce97d/1a5db30/14c06c8 contenevano migrazioni+HTML ma gli Edit dei
metodi controller e delle route erano falliti silenziosamente (ancore errate).
Ora presenti e testati E2E in produzione:
- DashboardController::sectorBenchmark (era 501)
- SupplyChainController: sendQuestionnaire/publicQuestionnaire/submitPublicQuestionnaire/questionnaireStatus/resolveQuestionnaire + route 'supply-chain' (era 404)
- PolicyController: attest/attestations/versions/diff/pendingAttestations + snapshot in approve + route (era 404)
Test: benchmark 200, supplier flow send->submit(score 61)->dedup 409->DB risk_score=39,
policy approve->attest(coverage 50%)->bump v2.0->diff(+2/-1)->pending ricompare.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Il commit 372ccb5 aveva incluso versioni con Edit falliti (ancore errate):
- AuditController::controlsMonitoring ora effettivamente presente (era 501 in prod)
- ServicesController::openapi ora espone incidents-ingest/evidence-ingest/assets-ingest/controls-monitoring
- i18n.js: chiavi nel formato corretto {it,en} (risks.fair_tab/kri_tab, assets.import_btn, audit.monitoring_tab)
- help.js: sezione Monitoraggio Continuo in reports
Verificato in prod: openapi 4/4, controlsMonitoring/fairRegister/kri tutti 200.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Il commit 1be3bd0 conteneva migrazione 026 + FairService + route ma NON i metodi
computeFair/fairRegister/listKri/createKri/updateKri nel controller (Edit fallito
per ancora errata). Ora presenti e testati E2E in prod:
- FAIR compute ALE Monte Carlo (risk 432: ALE mean 174.806 EUR, deterministico)
- fairRegister portfolio ALE, KRI create/update/dashboard con semaforo amber->red
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Competizione coi GRC enterprise sul risk management quantitativo:
- FairService: simulazione Monte Carlo FAIR (PERT su TEF e Loss Magnitude),
ALE in EUR con percentili P10/P50/P90 + istogramma, deterministico (seed da input)
- RiskController::computeFair -> POST /risks/{id}/fair (persiste parametri+ALE)
- RiskController::fairRegister -> GET /risks/fairRegister (portfolio ALE EUR)
- KRI: listKri/createKri/updateKri (GET/POST /risks/kri, PUT /risks/kri/{id})
con stato semaforo green/amber/red su soglie+direzione
- Migrazione 026: risks += parametri FAIR + ale_min/ml/max/mean; nuova tabella kri
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Il FAB ARIA (common.js) chiamava POST /api/ai/ask ma il controller non esisteva
(assistente AI rotto). Creato AiController::ask -> AIService::askWithRag con RAG su KB
+ grounding fonti certe. Verificato in produzione: rag_used=True, cita Ambiti NIS2 / Determina ACN.
Fix DNS Qdrant: nei worker php-fpm (musl) getenv e gethostbyname NON funzionano per
hostname Docker single-label; funziona solo un IP letterale. VectorService fallback ->
172.21.0.3 (fpm-safe); QDRANT_URL compose resta hostname per CLI. Vedi nota drift in VectorService.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
L'IP hardcoded Qdrant 172.21.0.5 era driftato a .3 (container senza IP statico) e
con php-fpm clear_env=no la env QDRANT_URL=172.21.0.5 (morta) veniva usata -> RAG web rotta.
Fix: QDRANT_URL e fallback VectorService usano l'hostname http://nis2-qdrant:6333,
risolto via Docker DNS sia in CLI sia in php-fpm. Verificato retrieval end-to-end (287 chunk).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ServicesController: nuovo endpoint GET /api/services/full-snapshot
Aggrega gap-analysis, measures, incidents, training, deadlines,
compliance-summary in una sola chiamata (reduce 6 round-trip → 1)
Parametro ?days=N per finestra deadlines (default 30, max 365)
- public/index.php: route GET:fullSnapshot aggiunta all'action map services
- public/simulate-nis2-big.php: wrapper SSE per simulate-nis2-big.php
Esegue il simulatore come sottoprocesso CLI con NIS2_SSE=1 e
streama l'output al browser tramite Server-Sent Events
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GET /services/gap-analysis — gap per dominio NIS2 Art.21 con mapping MOG 231 pillars
- GET /services/measures — compliance_controls con mog_area e nis2_article derivati
- GET /services/incidents — incidenti con Art.23 CSIRT compliance per step (24h/72h/30d)
- GET /services/training — corsi + completamento board (Art.20 compliance flag)
- GET /services/deadlines — scadenze aggregate da 4 sorgenti con ?days= filter
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- help.js: nuova sezione 'feedback' con 6 sotto-sezioni (come usare FAB,
risposta AI, password gate, le mie segnalazioni, worker autonomo, consigli)
- i18n.js: 30 chiavi IT/EN per tutto il sistema feedback
- AIService::callAPI: system prompt esteso con lista completa moduli NIS2 Agile
- AIService::classifyFeedback: system prompt NIS2-aware
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>