requireAuth(); // Risoluzione org OPZIONALE (il catalogo è visibile anche senza org), con // verifica d'accesso per non esporre lo stato di un'altra org (anti-IDOR). $orgId = null; $orgClass = null; if (!empty($this->isDemo)) { $orgId = $this->demoOrgId ?? null; } else { $candidate = $this->resolveOrgId(); if ($candidate) { if (($this->currentUser['role'] ?? '') === 'super_admin') { $orgId = $candidate; } else { $member = Database::fetchOne( 'SELECT 1 FROM user_organizations WHERE user_id = ? AND organization_id = ?', [$this->getCurrentUserId(), $candidate] ); if ($member) { $orgId = $candidate; } } } } if ($orgId) { $row = Database::fetchOne('SELECT entity_type FROM organizations WHERE id = ?', [$orgId]); $orgClass = $row['entity_type'] ?? null; // essential | important | not_applicable } // Stato per-org dei requisiti (mappa requisito_id => stato) $stateMap = []; if ($orgId) { foreach (Database::fetchAll( 'SELECT requisito_id, stato, valutazione_rischio, note, updated_at FROM org_requisito_state WHERE organization_id = ?', [$orgId] ) as $s) { $stateMap[(int) $s['requisito_id']] = [ 'stato' => $s['stato'], 'valutazione_rischio' => $s['valutazione_rischio'], 'note' => $s['note'], 'updated_at' => $s['updated_at'], ]; } } $ambiti = Database::fetchAll('SELECT nist_code, nist_descr FROM cfg_nis2_ambiti ORDER BY nist_code ASC'); $misure = Database::fetchAll( 'SELECT misura_code, nist_code, area_politica, misura_descr, ord, applies_important, applies_essential FROM cfg_nis2_misure ORDER BY ord ASC' ); $reqs = Database::fetchAll( 'SELECT q.id, q.n, q.misura_code, q.requisito_code, q.requisito_descr, q.proc_code, p.proc_descr, q.risk_code, r.risk_descr, q.applies_important, q.applies_essential FROM cfg_nis2_requisiti q LEFT JOIN cfg_nis2_procedure p ON p.proc_code = q.proc_code LEFT JOIN cfg_nis2_rischi r ON r.risk_code = q.risk_code ORDER BY q.n ASC, q.id ASC' ); $byMis = []; foreach ($reqs as $q) { $rid = (int) $q['id']; $st = $stateMap[$rid] ?? null; $byMis[$q['misura_code']][] = [ 'id' => $rid, 'n' => $q['n'] !== null ? (int) $q['n'] : null, 'requisito_code' => $q['requisito_code'], 'requisito_descr' => $q['requisito_descr'], 'proc_code' => $q['proc_code'], 'proc_descr' => $q['proc_descr'], 'risk_code' => $q['risk_code'], 'risk_descr' => $q['risk_descr'], 'applies_important' => (int) $q['applies_important'], 'applies_essential' => (int) $q['applies_essential'], 'stato' => $st['stato'] ?? 'da_valutare', 'valutazione_rischio' => $st['valutazione_rischio'] ?? null, 'note' => $st['note'] ?? null, ]; } $measures = []; foreach ($misure as $m) { $measures[] = [ 'misura_code' => $m['misura_code'], 'nist_code' => $m['nist_code'], 'area_politica' => $m['area_politica'], 'misura_descr' => $m['misura_descr'], 'ord' => (int) $m['ord'], 'applies_important' => (int) $m['applies_important'], 'applies_essential' => (int) $m['applies_essential'], 'requisiti' => $byMis[$m['misura_code']] ?? [], ]; } $this->jsonSuccess([ 'org_class' => $orgClass, 'has_org' => $orgId !== null, 'stati' => self::STATI, 'ambiti' => $ambiti, 'measures' => $measures, 'totals' => ['misure' => count($measures), 'requisiti' => count($reqs)], 'readonly' => true, 'source' => 'Determinazione ACN n. 164179 del 14 aprile 2025', ]); } /** * POST /api/framework/state * Upsert della valutazione/stato di UN requisito per l'org corrente. * Body: {requisito_id*, stato?, valutazione_rischio?, note?} * La codifica del framework NON e' toccata: si scrive solo org_requisito_state. */ public function setState(): void { $this->requireOrgRole(self::MANAGE_ROLES); $orgId = $this->getCurrentOrgId(); $body = $this->getJsonBody(); $reqId = isset($body['requisito_id']) ? (int) $body['requisito_id'] : 0; if ($reqId <= 0) { $this->jsonError('requisito_id mancante o non valido', 400); return; } // anti-IDOR: il requisito deve esistere nel catalogo canonico $exists = Database::fetchOne('SELECT id FROM cfg_nis2_requisiti WHERE id = ?', [$reqId]); if (!$exists) { $this->jsonError('Requisito inesistente', 404); return; } $stato = $body['stato'] ?? null; if ($stato !== null && !in_array($stato, self::STATI, true)) { $this->jsonError('Stato non valido', 400); return; } $valutazione = array_key_exists('valutazione_rischio', $body) ? ($body['valutazione_rischio'] !== '' ? (string) $body['valutazione_rischio'] : null) : null; $note = array_key_exists('note', $body) ? ($body['note'] !== '' ? (string) $body['note'] : null) : null; // upsert org-scoped (UNIQUE org+requisito) Database::query( 'INSERT INTO org_requisito_state (organization_id, requisito_id, stato, valutazione_rischio, note, updated_by) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE stato = VALUES(stato), valutazione_rischio = VALUES(valutazione_rischio), note = VALUES(note), updated_by = VALUES(updated_by)', [$orgId, $reqId, $stato ?? 'da_valutare', $valutazione, $note, $this->getCurrentUserId()] ); $row = Database::fetchOne( 'SELECT requisito_id, stato, valutazione_rischio, note, updated_at FROM org_requisito_state WHERE organization_id = ? AND requisito_id = ?', [$orgId, $reqId] ); $this->jsonSuccess([ 'requisito_id' => (int) $row['requisito_id'], 'stato' => $row['stato'], 'valutazione_rischio' => $row['valutazione_rischio'], 'note' => $row['note'], 'updated_at' => $row['updated_at'], ], 'Valutazione salvata'); } }