nis2-agile/application/services/AssetScoringService.php
DevEnv nis2-agile 5c545ea3d0 [FEAT] Integrazione analisi docs/nis2 v1.7.0 — scoring asset, tassonomia incidenti, PIR, NIST CSF, fonti certe
Fase 1 - Asset Relevance Scoring NIS2 (GV.OC-04): metodologia 0-100 a 6 criteri,
  AssetScoringService + endpoint scoringGrid/score/relevantSystems + UI assets.html + registro stampabile.
Fase 2 - Tassonomia incidenti Determina ACN 164179/2025: IS-1..4 + regime essenziale/importante (Allegati 3/4).
Fase 3 - Post-Incident Review (5-Whys) + metriche TTD/TTC/TTR + timestamp di fase.
Fase 4 - Mapping NIST CSF 2.0 (43 controlli) reference-only.
Fonti certe: registry config/nis2_sources.php + grounding AI (vieta riferimenti inventati) +
  citazioni help.js + ingest PDF normativi nella KB RAG (scripts/ingest-nis2-sources.php).
Migrazioni 020/021/022 (additive idempotenti). Fix VectorService IP Qdrant (drift .5->.3).
Analisi concorrenza Evix (docs/EVIX_ANALISI_CONCORRENZA.html, gap-driven).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 17:15:13 +02:00

203 lines
8.9 KiB
PHP

<?php
/**
* NIS2 Agile - Asset Scoring Service
*
* Metodologia di scoring rilevanza NIS2 (requisito GV.OC-04).
* Adattata dai mockup docs/nis2/assets.html + doc-relevant-systems.html.
*
* 6 criteri pesati, punteggio 0-100:
* C1 Criticita Operativa 0-25
* C2 Impatto Interruzione 0-25
* C3 Dati Trattati 0-20
* C4 Dipendenze 0-15
* C5 Esposizione 0-10
* C6 Obblighi Normativi 0-5
*
* Soglia rilevanza NIS2: score >= 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] ?? [];
}
}