Manuale Operativo Tecnico
UnicoWhistle — Piattaforma di whistleblowing conforme al D.Lgs. 24/2023 (attuazione della Direttiva UE 2019/1937)
1. Introduzione e scopo
UnicoWhistle è un'applicazione Laravel che implementa un canale di segnalazione interno (whistleblowing) ad uso multi-azienda: ogni azienda cliente (tenant) dispone di un proprio canale pubblico anonimo per la ricezione delle segnalazioni e di un pannello di gestione riservato ai gestori incaricati (gestori delle segnalazioni).
Il sistema automatizza gli adempimenti previsti dalla normativa italiana sul whistleblowing: avviso di ricevimento entro 7 giorni, riscontro finale entro 3 mesi, conservazione limitata nel tempo dei dati con anonimizzazione automatica, e tracciabilità delle comunicazioni tra segnalante e gestore mantenendo l'anonimato del primo.
2. Architettura tecnica
| Livello | Tecnologia | Note |
|---|---|---|
| Framework applicativo | Laravel 12 (PHP 8.4) | Struttura file standard Laravel 11+, configurazione middleware/eccezioni in bootstrap/app.php. |
| Pannello amministrativo | Filament v5 | Risorse in app/Filament/Resources, provider in app/Providers/Filament/AdminPanelProvider.php. |
| Form pubblici | Livewire | app/Livewire/PublicReportForm.php e PublicReportTracker.php, layout layouts.guest. |
| Allegati | Spatie Media Library | Collection evidence sul modello Report, disco private, contenuto cifrato. |
| Audit trail | Spatie Activitylog | Traccia solo i cambi di campi non sensibili (es. stato), mai il contenuto in chiaro. |
| barryvdh/laravel-dompdf | Esportazione del fascicolo segnalazione. | |
| 2FA | Filament Multi-Factor (TOTP) | Filament\Auth\MultiFactor\App\AppAuthentication. |
3. Modello dati
Company (azienda / tenant)
Tabella companies. Campi principali: name, slug (usato negli URL pubblici), logo_path, brand_color (white-label del pannello e delle pagine pubbliche), shared_passcode (codice opzionale condiviso per sbloccare il form pubblico) e passcode_rotated_at.
shared_passcode non viene mai scritto nell'audit log: viene tracciata solo la data
dell'ultima rotazione (passcode_rotated_at), aggiornata automaticamente tramite un mutatore
sull'attributo quando il valore cambia realmente.
User (gestore)
Tabella users. Relazione belongsToMany(Company): un gestore può essere assegnato a una o più aziende
(multi-tenancy Filament tramite HasTenants). canAccessPanel() richiede almeno un'azienda assegnata.
Autenticazione a due fattori TOTP tramite InteractsWithAppAuthentication.
Report (segnalazione)
| Campo | Tipo / cast | Note |
|---|---|---|
| tracking_token | string | PIN univoco nel formato WHSL-XXXX-XXXX, unico strumento con cui il segnalante accede in seguito alla propria pratica. |
| status | enum ReportStatus | Nuova In Lavorazione Chiusa |
| description | string, cast encrypted | Contenuto cifrato a riposo nel database. |
| corrective_measures | string, cast encrypted | Registro delle misure correttive adottate, cifrato. |
| acknowledgement_due_at | datetime | Impostata automaticamente a created_at + 7 giorni alla creazione. |
| feedback_due_at | datetime | Impostata automaticamente a created_at + 3 mesi alla creazione. |
| anonymized_at | datetime, nullable | Valorizzata da anonymize() quando la pratica supera il periodo di conservazione. |
Gli allegati sono gestiti tramite la collection Media Library evidence, salvati sul disco private e cifrati a riposo (vedi sezione Sicurezza).
Message (messaggio di chat)
Tabella messages, relazione hasMany su Report. Campo body cifrato, is_from_reporter (booleano) distingue i messaggi del segnalante da quelli del gestore. Non sono modificabili né eliminabili una volta creati (integrità dell'audit trail), imposto a livello di MessagePolicy.
CorrectiveMeasureTemplate (modello di misura correttiva)
Tabella corrective_measure_templates. Campo company_id nullable: se null il modello è globale e visibile/riutilizzabile da tutte le aziende, altrimenti è specifico dell'azienda proprietaria. La modifica dei template globali è riservata alla console/seeder (vedi CorrectiveMeasureTemplatePolicy).
4. Multi-tenancy
Il pannello Filament è configurato con ->tenant(Company::class, slugAttribute: 'slug') in
AdminPanelProvider: ogni URL del pannello è prefissato dallo slug dell'azienda selezionata e
lo scoping automatico delle query per tenant è delegato a Filament, tranne dove esplicitamente
disabilitato (es. CorrectiveMeasureTemplateResource, che ha bisogno di mostrare anche i
modelli globali con company_id nullo, e ricostruisce lo scoping a mano in
getEloquentQuery()).
Il colore primario dell'interfaccia (brand_color) viene applicato dinamicamente in base al tenant corrente tramite la closure passata a ->colors().
5. Sicurezza e protezione dati
Autenticazione a due fattori
La 2FA tramite app authenticator (TOTP) è obbligatoria per ogni gestore al primo accesso utile, tranne in locale con APP_DEBUG=true dove resta disponibile ma non obbligatoria, per non intralciare lo sviluppo.
Cifratura dei dati
- Contenuto testuale:
descriptionecorrective_measuresdel Report, ebodydei Message, sono cifrati a riposo tramite cast Eloquentencrypted(basato suAPP_KEY). - Allegati: ogni file caricato dal segnalante viene cifrato con
Crypt::encryptString()prima di essere scritto sul discoprivate; nome file originale e mime type sono conservati solo come custom properties del record Media (nel database), mai nel filesystem in chiaro. - Passcode condiviso: confrontato con
hash_equals()per evitare timing attack; il valore non compare mai nei log applicativi.
Isolamento tenant e protezione IDOR
La rotta /admin/media/{media}/download verifica esplicitamente che l'utente autenticato
appartenga alla company proprietaria del Report a cui il media è associato, prima di decifrare e
restituire il file: previene l'accesso cross-tenant agli allegati anche conoscendo l'ID del media. La
rotta di esportazione PDF (/admin/reports/{report}/pdf) applica la policy view
del Report. Il tracciamento pubblico (PublicReportTracker) filtra sempre le segnalazioni con
lo scope forCompany(), impedendo di raggiungere segnalazioni di un'altra azienda anche
indovinando un PIN valido per un tenant diverso.
Rate limiting
| Azione | Chiave | Limite |
|---|---|---|
| Verifica passcode form pubblico | passcode-verify:{company}:{ip} | 10 tentativi / 60s |
| Invio segnalazione | report-submit:{company}:{ip} | 5 invii / 300s |
| Accesso tramite PIN | report-tracker:{company}:{ip} | 10 tentativi / 60s |
Audit trail
Tramite Spatie Activitylog, Company e Report registrano solo le modifiche ai campi
non sensibili (es. lo stato di una segnalazione, o la data di rotazione del passcode), mai il
contenuto cifrato. Le voci di log sono consultabili dal pannello nella scheda "Log attività" della
risorsa Segnalazioni.
ReportPolicy, MessagePolicy), non solo di interfaccia.
6. Flusso pubblico del segnalante
- Accesso al canale:
/segnala/{company:slug}. Se l'azienda ha impostato unshared_passcode, viene richiesto prima di mostrare il form. - Compilazione: oggetto, descrizione dettagliata, fino a 5 allegati (PDF, JPEG, PNG, MP3, WAV; max 10MB ciascuno) — inclusi eventuali messaggi vocali registrati e alterati nel timbro dal browser.
- Invio: viene generato un PIN univoco
WHSL-XXXX-XXXX, la segnalazione è salvata con stato "Nuova" e i gestori dell'azienda ricevono una notifica via email priva del contenuto della segnalazione (solo invito ad accedere al pannello). - Tracciamento:
/traccia/{company:slug}, accesso tramite il PIN ricevuto. Il segnalante può leggere e inviare messaggi in chat con il gestore, restando anonimo, e può richiedere un incontro diretto (notifica ai gestori).
7. Flusso operativo del gestore
- Login sul pannello (
/admin) con verifica 2FA TOTP. - Selezione dell'azienda (tenant) se assegnato a più aziende.
- Dashboard con widget contatori (Nuove / In Lavorazione / Chiuse) scoperto per il tenant corrente.
- Risorsa Segnalazioni: elenco con colonne PIN, oggetto, stato, avviso/riscontro scaduti, incontro richiesto. Apertura di una pratica per: aggiornare lo stato, registrare le misure correttive (anche richiamando un
CorrectiveMeasureTemplate), inviare l'avviso di ricevimento, esportare il fascicolo in PDF, rispondere in chat al segnalante. - Risorsa Aziende (solo Amministrazione SaaS): anagrafica, logo, colore brand, generazione/rotazione del passcode condiviso, generazione del QR code del link pubblico di segnalazione.
- Risorsa Utenti (solo Amministrazione SaaS): creazione gestori e assegnazione alle aziende di competenza.
- Risorsa Modelli Misure Correttive: gestione dei testi predefiniti richiamabili nel registro delle misure correttive, globali o specifici per azienda.
8. Scadenziario di legge automatizzato
Alla creazione di ogni Report, il sistema calcola automaticamente due scadenze previste dal D.Lgs. 24/2023:
| Adempimento | Termine | Campo |
|---|---|---|
| Avviso di ricevimento al segnalante | 7 giorni dalla ricezione | acknowledgement_due_at |
| Riscontro finale sull'esito della segnalazione | 3 mesi dalla ricezione | feedback_due_at |
Le scadenze superate o in avvicinamento sono evidenziate nella tabella delle Segnalazioni e generano promemoria automatici ai gestori (vedi sezione successiva).
9. Job pianificati e comandi Artisan
| Comando | Pianificazione | Funzione |
|---|---|---|
| reports:check-deadlines | Giornaliera | Invia promemoria ai gestori: avviso di ricevimento scaduto o in scadenza entro 2 giorni; riscontro finale scaduto o in scadenza entro 14 giorni. Evita duplicati tramite i timestamp *_reminder_sent_at. |
| reports:anonymize-expired | Giornaliera | Anonimizza le segnalazioni chiuse da più tempo del periodo di conservazione configurato (contenuto, allegati e messaggi vengono cancellati, mantenendo solo i metadati necessari a statistiche aggregate e audit). |
La pianificazione è definita in routes/console.php tramite la facade Schedule; è necessario che lo scheduler Laravel (php artisan schedule:run) sia attivo via cron/supervisor in produzione.
10. Configurazione d'ambiente
| Variabile | Descrizione | Default |
|---|---|---|
| APP_NAME | Nome applicazione mostrato in email e pannello. | UnicoWhistle |
| REPORT_RETENTION_MONTHS | Mesi di conservazione di una segnalazione chiusa prima dell'anonimizzazione automatica. | 24 |
| APP_DEBUG | Se true, disabilita l'obbligo di 2FA (solo per sviluppo locale). | false in produzione |
Il file config/whistleblowing.php centralizza le impostazioni specifiche di dominio (attualmente retention_months).
11. Manutenzione operativa
- Verificare periodicamente che lo scheduler (
schedule:run) sia in esecuzione: da esso dipendono sia i promemoria di legge sia l'anonimizzazione automatica, entrambi requisiti di conformità. - Il disco
privatedeve restare non accessibile pubblicamente: gli allegati sono cifrati, ma il percorso di storage non deve comunque essere esposto da configurazioni del webserver. - Eventuali interventi su
APP_KEY(rotazione della chiave) rendono illeggibili i dati cifrati con la chiave precedente (descrizioni, misure correttive, messaggi, allegati): pianificare sempre una migrazione dei dati cifrati prima di sostituire la chiave. - Il pannello richiede almeno un'azienda assegnata per consentire l'accesso a un utente (
canAccessPanel()): un gestore senza aziende assegnate non può accedere, anche con credenziali valide.