= 40. * Classi: >=80 critico | 60-79 alto | 40-59 medio | 20-39 basso | <20 trascurabile. * * La logica e' PURA (nessun side effect / DB): si presta a unit test e riuso. */ class AssetScoringService { public const RELEVANCE_THRESHOLD = 40; /** * Griglia ufficiale: per ogni criterio, la lista di opzioni selezionabili * (value => punti) con label per la UI. value e' una chiave stabile usata * dal frontend e salvata in relevance_criteria JSON. */ public const GRID = [ 'c1_operational_criticality' => [ 'label' => 'Criticita Operativa', 'max' => 25, 'help' => 'Quanto il sistema e essenziale per l\'erogazione dei servizi core business.', 'options' => [ 'critical' => ['label' => 'Critico', 'points' => 25], 'very_high' => ['label' => 'Molto Alto', 'points' => 20], 'high' => ['label' => 'Alto', 'points' => 15], 'medium' => ['label' => 'Medio', 'points' => 10], 'low' => ['label' => 'Basso', 'points' => 5], 'negligible' => ['label' => 'Trascurabile', 'points' => 0], ], ], 'c2_disruption_impact' => [ 'label' => 'Impatto Interruzione', 'max' => 25, 'help' => 'Conseguenze di un\'interruzione in termini di durata e utenti impattati.', 'options' => [ 'gt24h_gt70' => ['label' => '>24h + >70% utenti', 'points' => 25], 'h8_24_50_70' => ['label' => '8-24h + 50-70% utenti', 'points' => 20], 'h4_8_30_50' => ['label' => '4-8h + 30-50% utenti', 'points' => 15], 'h1_4_10_30' => ['label' => '1-4h + 10-30% utenti', 'points' => 10], 'lt1h_lt10' => ['label' => '<1h + <10% utenti', 'points' => 5], 'none' => ['label' => 'Nessun impatto', 'points' => 0], ], ], 'c3_data_processed' => [ 'label' => 'Dati Trattati', 'max' => 20, 'help' => 'Sensibilita e criticita dei dati gestiti dal sistema.', 'options' => [ 'gdpr_art9' => ['label' => 'Dati Sensibili Art.9 GDPR', 'points' => 20], 'personal_large' => ['label' => 'Dati Personali larga scala', 'points' => 15], 'personal_fin' => ['label' => 'Dati Personali + Finanziari', 'points' => 10], 'confidential' => ['label' => 'Dati Aziendali Riservati', 'points' => 5], 'public' => ['label' => 'Dati Pubblici', 'points' => 0], ], ], 'c4_dependencies' => [ 'label' => 'Dipendenze', 'max' => 15, 'help' => 'Quanti altri sistemi critici dipendono da questo sistema.', 'options' => [ 'ge5_critical' => ['label' => '>=5 sistemi critici', 'points' => 15], 'n3_4_critical' => ['label' => '3-4 sistemi critici', 'points' => 12], 'n2_critical' => ['label' => '2 sistemi critici', 'points' => 9], 'n1_critical' => ['label' => '1 sistema critico', 'points' => 6], 'noncritical' => ['label' => '1-2 sistemi non critici', 'points' => 3], 'none' => ['label' => 'Nessuna dipendenza', 'points' => 0], ], ], 'c5_exposure' => [ 'label' => 'Esposizione', 'max' => 10, 'help' => 'Superficie di attacco ed esposizione del sistema.', 'options' => [ 'internet_no_mfa' => ['label' => 'Internet pubblico senza MFA', 'points' => 10], 'internet_mfa' => ['label' => 'Internet con MFA', 'points' => 8], 'partner_net' => ['label' => 'Reti partner/fornitori', 'points' => 6], 'intranet' => ['label' => 'Rete aziendale intranet', 'points' => 4], 'mgmt_isolated' => ['label' => 'Rete gestione isolata', 'points' => 2], 'air_gapped' => ['label' => 'Completamente isolato', 'points' => 0], ], ], 'c6_regulatory' => [ 'label' => 'Obblighi Normativi', 'max' => 5, 'help' => 'Se il sistema e soggetto a obblighi normativi o contrattuali specifici.', 'options' => [ 'nis2_required' => ['label' => 'Richiesto da NIS2', 'points' => 5], 'mandatory_cert' => ['label' => 'Certificazioni obbligatorie', 'points' => 4], 'strict_sla' => ['label' => 'Obblighi SLA stringenti', 'points' => 3], 'external_audit' => ['label' => 'Audit esterni regolari', 'points' => 2], 'none' => ['label' => 'Nessun obbligo', 'points' => 0], ], ], ]; /** * Calcola lo score a partire dalle selezioni dell'utente. * * @param array $criteria mappa criterioKey => optionValue * es. ['c1_operational_criticality' => 'critical', ...] * @return array{score:int, class:string, is_relevant:bool, breakdown:array, criticality:string} * @throws InvalidArgumentException se un criterio/opzione non e valido */ public static function calculate(array $criteria): array { $score = 0; $breakdown = []; foreach (self::GRID as $key => $def) { if (!array_key_exists($key, $criteria)) { throw new InvalidArgumentException("Criterio mancante: {$key}"); } $optVal = $criteria[$key]; if (!isset($def['options'][$optVal])) { throw new InvalidArgumentException("Opzione non valida '{$optVal}' per criterio {$key}"); } $pts = $def['options'][$optVal]['points']; $score += $pts; $breakdown[$key] = [ 'value' => $optVal, 'label' => $def['options'][$optVal]['label'], 'points' => $pts, 'max' => $def['max'], ]; } $class = self::classify($score); return [ 'score' => $score, 'class' => $class, 'is_relevant' => $score >= self::RELEVANCE_THRESHOLD, 'breakdown' => $breakdown, // mapping verso l'enum legacy assets.criticality per coerenza UI esistente 'criticality' => self::toLegacyCriticality($score), ]; } /** Classe testuale secondo le soglie ufficiali. */ public static function classify(int $score): string { if ($score >= 80) return 'critico'; if ($score >= 60) return 'alto'; if ($score >= 40) return 'medio'; if ($score >= 20) return 'basso'; return 'trascurabile'; } /** Allinea lo score all'enum assets.criticality preesistente (low/medium/high/critical). */ public static function toLegacyCriticality(int $score): string { if ($score >= 80) return 'critical'; if ($score >= 60) return 'high'; if ($score >= 40) return 'medium'; return 'low'; } /** Misure obbligatorie associate alla classe (per report GV.OC-04 / UI). */ public static function requiredMeasures(string $class): array { return [ 'critico' => [ 'Monitoraggio continuo 24/7 (SIEM/SOC)', 'Backup immutabile con test ripristino periodico', 'MFA obbligatoria + accessi privilegiati controllati (PAM)', 'Inclusione obbligatoria in BIA e piano di continuita', 'Test di vulnerabilita/penetration test almeno annuali', ], 'alto' => [ 'Monitoraggio in orario lavorativo + alerting', 'Backup regolari con verifica integrita', 'MFA per accessi remoti', 'Inclusione in risk assessment ciclico', ], 'medio' => [ 'Logging centralizzato', 'Backup periodici', 'Patch management documentato', ], 'basso' => [ 'Inventario aggiornato', 'Patch management standard', ], 'trascurabile' => [ 'Censimento in inventario asset', ], ][$class] ?? []; } }