nis2-agile/application/controllers/AssetController.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

278 lines
10 KiB
PHP

<?php
/**
* NIS2 Agile - Asset Controller
*
* Inventario asset IT/OT, classificazione, dipendenze.
*/
require_once __DIR__ . '/BaseController.php';
require_once __DIR__ . '/../services/AssetScoringService.php';
class AssetController extends BaseController
{
public function list(): void
{
$this->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,
]);
}
}