[FEAT] Asset import CMDB/cloud + scoring automatico GV.OC-04 (P2)
- AssetScoringService::inferCriteria: euristica 6 criteri da campi CMDB (criticality, data_classification, internet_facing, dependencies, regulated) - AssetController::import (JWT org_admin/compliance_manager) + bulkUpsert condiviso: upsert dedup su external_ref, scoring auto GV.OC-04, max 1000 asset/batch - ServicesController::ingestAssets -> POST /services/assets-ingest (scope ingest:assets) per connettori CMDB/cloud - Migrazione 025: assets += external_ref + discovery_source + indice univoco dedup - Route POST:assetsIngest (services) + POST:import (assets) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
307993fbad
commit
4924075142
@@ -76,6 +76,109 @@ class AssetController extends BaseController
|
||||
$this->jsonSuccess(['id' => $assetId], 'Asset registrato', 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/assets/import (JWT, org_admin/compliance_manager)
|
||||
* Import bulk asset da CMDB/cloud/CSV con scoring automatico GV.OC-04.
|
||||
* Body: { "source":"cmdb|aws|azure|csv|manual", "assets":[ {...}, ... ] }
|
||||
*/
|
||||
public function import(): void
|
||||
{
|
||||
$this->requireOrgRole(['org_admin', 'compliance_manager']);
|
||||
$body = $this->getJsonBody();
|
||||
$source = strtolower((string) ($body['source'] ?? 'csv'));
|
||||
$items = $body['assets'] ?? null;
|
||||
if (!is_array($items) || !$items) {
|
||||
$this->jsonError('Campo "assets" (array) obbligatorio', 422, 'VALIDATION');
|
||||
}
|
||||
$result = self::bulkUpsert($this->getCurrentOrgId(), $items, $source, $this->getCurrentUserId());
|
||||
$this->logAudit('assets_imported', 'asset', null, [
|
||||
'source' => $source, 'imported' => $result['imported'], 'updated' => $result['updated'],
|
||||
]);
|
||||
$this->jsonSuccess($result, 'Import completato', 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert bulk + scoring GV.OC-04. Condiviso fra import UI (JWT) e
|
||||
* ingestion connettori (API key, ServicesController). Riceve orgId esplicito.
|
||||
*
|
||||
* @return array{imported:int,updated:int,skipped:int,relevant:int,total:int,results:array}
|
||||
*/
|
||||
public static function bulkUpsert(int $orgId, array $items, string $source, ?int $userId): array
|
||||
{
|
||||
$validType = ['hardware', 'software', 'network', 'data', 'service', 'personnel', 'facility'];
|
||||
$imported = 0; $updated = 0; $skipped = 0; $relevant = 0; $results = [];
|
||||
|
||||
if (count($items) > 1000) {
|
||||
$items = array_slice($items, 0, 1000);
|
||||
}
|
||||
|
||||
foreach ($items as $i => $a) {
|
||||
if (!is_array($a)) { $skipped++; $results[] = ['index' => $i, 'ok' => false, 'error' => 'not_an_object']; continue; }
|
||||
$name = trim((string) ($a['name'] ?? ''));
|
||||
if ($name === '') { $skipped++; $results[] = ['index' => $i, 'ok' => false, 'error' => 'name mancante']; continue; }
|
||||
|
||||
$type = strtolower((string) ($a['asset_type'] ?? 'service'));
|
||||
if (!in_array($type, $validType, true)) $type = 'service';
|
||||
|
||||
// Scoring automatico GV.OC-04 da euristica sui campi CMDB
|
||||
$criteria = AssetScoringService::inferCriteria($a);
|
||||
$sc = AssetScoringService::calculate($criteria);
|
||||
|
||||
$extRef = isset($a['external_ref']) ? substr(trim((string) $a['external_ref']), 0, 190) : null;
|
||||
|
||||
$row = [
|
||||
'organization_id' => $orgId,
|
||||
'name' => $name,
|
||||
'asset_type' => $type,
|
||||
'category' => $a['category'] ?? null,
|
||||
'description' => $a['description'] ?? null,
|
||||
'criticality' => $sc['criticality'],
|
||||
'location' => $a['location'] ?? null,
|
||||
'ip_address' => $a['ip_address'] ?? null,
|
||||
'vendor' => $a['vendor'] ?? null,
|
||||
'discovery_source' => substr($source, 0, 40),
|
||||
'external_ref' => $extRef,
|
||||
'relevance_score' => $sc['score'],
|
||||
'relevance_criteria' => json_encode($criteria, JSON_UNESCAPED_UNICODE),
|
||||
'relevance_class' => $sc['class'],
|
||||
'is_nis2_relevant' => $sc['is_relevant'] ? 1 : 0,
|
||||
'relevance_assessed_at' => date('Y-m-d H:i:s'),
|
||||
'relevance_assessed_by' => $userId,
|
||||
];
|
||||
|
||||
try {
|
||||
$existing = $extRef !== null
|
||||
? Database::fetchOne('SELECT id FROM assets WHERE organization_id = ? AND external_ref = ?', [$orgId, $extRef])
|
||||
: null;
|
||||
if ($existing) {
|
||||
$sets = []; $vals = [];
|
||||
foreach ($row as $k => $v) { if ($k === 'organization_id') continue; $sets[] = "$k = ?"; $vals[] = $v; }
|
||||
$vals[] = $existing['id'];
|
||||
Database::query('UPDATE assets SET ' . implode(', ', $sets) . ' WHERE id = ?', $vals);
|
||||
$assetId = (int) $existing['id']; $updated++;
|
||||
} else {
|
||||
$assetId = Database::insert('assets', $row); $imported++;
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$skipped++; $results[] = ['index' => $i, 'ok' => false, 'error' => 'db_error'];
|
||||
error_log('[ASSET_IMPORT] ' . $e->getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($sc['is_relevant']) $relevant++;
|
||||
$results[] = [
|
||||
'index' => $i, 'ok' => true, 'id' => $assetId, 'name' => $name,
|
||||
'relevance_score' => $sc['score'], 'relevance_class' => $sc['class'],
|
||||
'nis2_relevant' => $sc['is_relevant'],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'imported' => $imported, 'updated' => $updated, 'skipped' => $skipped,
|
||||
'relevant' => $relevant, 'total' => count($items), 'results' => $results,
|
||||
];
|
||||
}
|
||||
|
||||
public function get(int $id): void
|
||||
{
|
||||
$this->requireOrgAccess();
|
||||
|
||||
Reference in New Issue
Block a user