requireOrgAccess(); $pagination = $this->getPagination(); $where = 'organization_id = ?'; $params = [$this->getCurrentOrgId()]; if ($this->hasParam('asset_type')) { $where .= ' AND asset_type = ?'; $params[] = $this->getParam('asset_type'); } if ($this->hasParam('criticality')) { $where .= ' AND criticality = ?'; $params[] = $this->getParam('criticality'); } if ($this->hasParam('status')) { $where .= ' AND status = ?'; $params[] = $this->getParam('status'); } if ($this->hasParam('nis2_relevant')) { $where .= ' AND is_nis2_relevant = ?'; $params[] = $this->getParam('nis2_relevant') ? 1 : 0; } $total = Database::count('assets', $where, $params); $assets = Database::fetchAll( "SELECT a.*, u.full_name as owner_name FROM assets a LEFT JOIN users u ON u.id = a.owner_user_id WHERE a.{$where} ORDER BY a.criticality DESC, a.name LIMIT {$pagination['per_page']} OFFSET {$pagination['offset']}", $params ); $this->jsonPaginated($assets, $total, $pagination['page'], $pagination['per_page']); } public function create(): void { $this->requireOrgRole(['org_admin', 'compliance_manager']); $this->validateRequired(['name', 'asset_type']); $assetId = Database::insert('assets', [ 'organization_id' => $this->getCurrentOrgId(), 'name' => trim($this->getParam('name')), 'asset_type' => $this->getParam('asset_type'), 'category' => $this->getParam('category'), 'description' => $this->getParam('description'), 'criticality' => $this->getParam('criticality', 'medium'), 'owner_user_id' => $this->getParam('owner_user_id'), 'location' => $this->getParam('location'), 'ip_address' => $this->getParam('ip_address'), 'vendor' => $this->getParam('vendor'), 'version' => $this->getParam('version'), 'serial_number' => $this->getParam('serial_number'), 'purchase_date' => $this->getParam('purchase_date'), 'warranty_expiry' => $this->getParam('warranty_expiry'), 'dependencies' => $this->getParam('dependencies') ? json_encode($this->getParam('dependencies')) : null, ]); $this->logAudit('asset_created', 'asset', $assetId); $this->jsonSuccess(['id' => $assetId], 'Asset registrato', 201); } public function get(int $id): void { $this->requireOrgAccess(); $asset = Database::fetchOne( 'SELECT a.*, u.full_name as owner_name FROM assets a LEFT JOIN users u ON u.id = a.owner_user_id WHERE a.id = ? AND a.organization_id = ?', [$id, $this->getCurrentOrgId()] ); if (!$asset) { $this->jsonError('Asset non trovato', 404, 'ASSET_NOT_FOUND'); } $this->jsonSuccess($asset); } public function update(int $id): void { $this->requireOrgRole(['org_admin', 'compliance_manager']); $updates = []; $fields = ['name', 'asset_type', 'category', 'description', 'criticality', 'owner_user_id', 'location', 'ip_address', 'vendor', 'version', 'serial_number', 'purchase_date', 'warranty_expiry', 'status']; foreach ($fields as $field) { if ($this->hasParam($field)) { $updates[$field] = $this->getParam($field); } } if ($this->hasParam('dependencies')) { $updates['dependencies'] = json_encode($this->getParam('dependencies')); } if (!empty($updates)) { Database::update('assets', $updates, 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]); $this->logAudit('asset_updated', 'asset', $id, $updates); } $this->jsonSuccess($updates, 'Asset aggiornato'); } public function delete(int $id): void { $this->requireOrgRole(['org_admin']); $deleted = Database::delete('assets', 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]); if ($deleted === 0) { $this->jsonError('Asset non trovato', 404, 'ASSET_NOT_FOUND'); } $this->logAudit('asset_deleted', 'asset', $id); $this->jsonSuccess(null, 'Asset eliminato'); } public function dependencyMap(): void { $this->requireOrgAccess(); $assets = Database::fetchAll( 'SELECT id, name, asset_type, criticality, dependencies FROM assets WHERE organization_id = ? AND status = "active"', [$this->getCurrentOrgId()] ); $nodes = []; $edges = []; foreach ($assets as $asset) { $nodes[] = [ 'id' => $asset['id'], 'label' => $asset['name'], 'type' => $asset['asset_type'], 'criticality' => $asset['criticality'], ]; $deps = json_decode($asset['dependencies'] ?? '[]', true) ?: []; foreach ($deps as $depId) { $edges[] = ['from' => $asset['id'], 'to' => (int) $depId]; } } $this->jsonSuccess(['nodes' => $nodes, 'edges' => $edges]); } /** * GET /api/assets/scoringGrid * Ritorna la griglia ufficiale di valutazione rilevanza NIS2 (GV.OC-04) * per costruire la UI di scoring lato client. */ public function scoringGrid(): void { $this->requireOrgAccess(); $this->jsonSuccess([ 'grid' => AssetScoringService::GRID, 'threshold' => AssetScoringService::RELEVANCE_THRESHOLD, 'classes' => [ ['key' => 'critico', 'min' => 80, 'max' => 100, 'label' => 'Critico - Priorita Massima'], ['key' => 'alto', 'min' => 60, 'max' => 79, 'label' => 'Alto - Priorita Alta'], ['key' => 'medio', 'min' => 40, 'max' => 59, 'label' => 'Medio - Rilevante'], ['key' => 'basso', 'min' => 20, 'max' => 39, 'label' => 'Basso - Monitoraggio'], ['key' => 'trascurabile', 'min' => 0, 'max' => 19, 'label' => 'Trascurabile'], ], ]); } /** * POST /api/assets/{id}/score * Calcola e salva la rilevanza NIS2 dell'asset a partire dalle selezioni * sui 6 criteri. Body: { criteria: { c1_operational_criticality: 'critical', ... } } */ public function score(int $id): void { $this->requireOrgRole(['org_admin', 'compliance_manager']); $asset = Database::fetchOne( 'SELECT id FROM assets WHERE id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()] ); if (!$asset) { $this->jsonError('Asset non trovato', 404, 'ASSET_NOT_FOUND'); } $criteria = $this->getParam('criteria'); if (!is_array($criteria)) { $this->jsonError('Campo "criteria" mancante o non valido', 422, 'INVALID_CRITERIA'); } try { $result = AssetScoringService::calculate($criteria); } catch (InvalidArgumentException $e) { $this->jsonError($e->getMessage(), 422, 'INVALID_CRITERIA'); return; } Database::update('assets', [ 'relevance_score' => $result['score'], 'relevance_criteria' => json_encode($result['breakdown'], JSON_UNESCAPED_UNICODE), 'relevance_class' => $result['class'], 'is_nis2_relevant' => $result['is_relevant'] ? 1 : 0, 'criticality' => $result['criticality'], 'relevance_assessed_at' => date('Y-m-d H:i:s'), 'relevance_assessed_by' => $this->getCurrentUserId(), ], 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]); $this->logAudit('asset_scored', 'asset', $id, [ 'score' => $result['score'], 'class' => $result['class'], ]); $this->jsonSuccess([ 'score' => $result['score'], 'class' => $result['class'], 'is_nis2_relevant' => $result['is_relevant'], 'breakdown' => $result['breakdown'], 'required_measures'=> AssetScoringService::requiredMeasures($result['class']), ], 'Rilevanza NIS2 calcolata'); } /** * GET /api/assets/relevantSystems * Elenco dei sistemi classificati rilevanti NIS2 (score >= 40), ordinati * per punteggio. Alimenta il registro formale "Sistemi Rilevanti" (GV.OC-04). */ public function relevantSystems(): void { $this->requireOrgAccess(); $rows = Database::fetchAll( "SELECT a.id, a.name, a.asset_type, a.category, a.ip_address, a.location, a.relevance_score, a.relevance_class, a.relevance_criteria, a.relevance_assessed_at, u.full_name AS owner_name FROM assets a LEFT JOIN users u ON u.id = a.owner_user_id WHERE a.organization_id = ? AND a.is_nis2_relevant = 1 ORDER BY a.relevance_score DESC, a.name", [$this->getCurrentOrgId()] ); $stats = ['critico' => 0, 'alto' => 0, 'medio' => 0]; foreach ($rows as &$r) { $r['relevance_criteria'] = json_decode($r['relevance_criteria'] ?? 'null', true); $r['required_measures'] = AssetScoringService::requiredMeasures($r['relevance_class'] ?? ''); if (isset($stats[$r['relevance_class']])) { $stats[$r['relevance_class']]++; } } unset($r); $this->jsonSuccess([ 'systems' => $rows, 'count' => count($rows), 'by_class' => $stats, 'threshold' => AssetScoringService::RELEVANCE_THRESHOLD, ]); } }