Compare commits
3 Commits
94d7867cea
...
21909994c2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21909994c2 | ||
|
|
094d453e8e | ||
|
|
d5d83bb3b9 |
75
application/controllers/AiController.php
Normal file
75
application/controllers/AiController.php
Normal file
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
/**
|
||||
* NIS2 Agile - AI Assistant Controller (ARIA)
|
||||
*
|
||||
* Endpoint dell'assistente conversazionale AI usato dal FAB "ARIA" (common.js).
|
||||
* Usa AIService::askWithRag() -> RAG su Knowledge Base (incl. fonti normative
|
||||
* certe ingerite, scope SYSTEM) + grounding con citazioni.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/BaseController.php';
|
||||
require_once __DIR__ . '/../services/AIService.php';
|
||||
require_once __DIR__ . '/../services/RateLimitService.php';
|
||||
|
||||
class AiController extends BaseController
|
||||
{
|
||||
/**
|
||||
* POST /api/ai/ask
|
||||
* Body: { question: string, history?: array }
|
||||
* Risposta: { answer, sources, rag_used }
|
||||
*/
|
||||
public function ask(): void
|
||||
{
|
||||
$this->requireAuth();
|
||||
|
||||
$question = trim((string) $this->getParam('question', ''));
|
||||
if ($question === '') {
|
||||
$this->jsonError('Domanda mancante', 422, 'QUESTION_REQUIRED');
|
||||
}
|
||||
if (mb_strlen($question) > 2000) {
|
||||
$this->jsonError('Domanda troppo lunga (max 2000 caratteri)', 422, 'QUESTION_TOO_LONG');
|
||||
}
|
||||
|
||||
// Rate limit per utente (riusa la soglia AI configurata)
|
||||
$userId = $this->getCurrentUserId();
|
||||
$rlKey = "ai_ask:{$userId}";
|
||||
if (defined('RATE_LIMIT_AI')) {
|
||||
RateLimitService::check($rlKey, RATE_LIMIT_AI);
|
||||
RateLimitService::increment($rlKey);
|
||||
}
|
||||
|
||||
$user = $this->getCurrentUser() ?? [];
|
||||
$userContext = [
|
||||
'user_id' => $userId,
|
||||
'organization_id' => $this->getCurrentOrgId(), // può essere null
|
||||
'consulting_firm_id' => $user['consulting_firm_id'] ?? null,
|
||||
];
|
||||
|
||||
try {
|
||||
$aiService = new AIService();
|
||||
$result = $aiService->askWithRag($question, $userContext);
|
||||
|
||||
// Audit best-effort (non blocca la risposta)
|
||||
try {
|
||||
$aiService->logInteraction(
|
||||
(int) ($userContext['organization_id'] ?? 0),
|
||||
(int) ($userId ?? 0),
|
||||
'qa',
|
||||
mb_substr($question, 0, 200),
|
||||
mb_substr((string) ($result['answer'] ?? ''), 0, 500),
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
error_log('[AiController::ask] logInteraction failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$this->jsonSuccess([
|
||||
'answer' => $result['answer'] ?? '',
|
||||
'sources' => $result['sources'] ?? [],
|
||||
'rag_used' => $result['rag_used'] ?? false,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
error_log('[AiController::ask] ' . $e->getMessage());
|
||||
$this->jsonError('Assistente AI non disponibile in questo momento', 503, 'AI_UNAVAILABLE');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2185,4 +2185,220 @@ class ServicesController extends BaseController
|
||||
'deadlines' => $deadlines,
|
||||
]);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// INGESTION INCIDENTI (P1) — apertura automatica incidenti Art.23 da
|
||||
// alert SIEM/SOC/EDR. Scope: ingest:incidents. Dedup su external_ref.
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* POST /api/services/incidents-ingest
|
||||
* Auth: X-API-Key con scope ingest:incidents
|
||||
* Body JSON (alert esterno):
|
||||
* {
|
||||
* "external_id": "splunk-8842", // per dedup (consigliato)
|
||||
* "source": "siem|soc|edr|api|email",
|
||||
* "source_system": "Splunk Enterprise",
|
||||
* "title": "Brute force su VPN gateway",
|
||||
* "description": "...",
|
||||
* "severity": "high" | 8 | "P1", // mappata su low/medium/high/critical
|
||||
* "detected_at": "2026-05-30T05:00:00Z",
|
||||
* "classification": "cyber_attack", // opzionale (altrimenti AI)
|
||||
* "ai_classify": true, // default true
|
||||
* "affected_services": "...", "affected_users_count": 0,
|
||||
* "cross_border_impact": false, "malicious_action": true
|
||||
* }
|
||||
*/
|
||||
public function ingestIncident(): void
|
||||
{
|
||||
$this->requireApiKey('ingest:incidents');
|
||||
$orgId = $this->currentOrgId;
|
||||
$body = $this->getJsonBody();
|
||||
|
||||
$title = trim((string) ($body['title'] ?? ''));
|
||||
if ($title === '') {
|
||||
$this->jsonError('Campo "title" obbligatorio', 422, 'VALIDATION');
|
||||
}
|
||||
|
||||
// Sorgente
|
||||
$source = strtolower((string) ($body['source'] ?? 'api'));
|
||||
if (!in_array($source, ['siem', 'soc', 'edr', 'api', 'email'], true)) {
|
||||
$source = 'api';
|
||||
}
|
||||
$sourceSystem = isset($body['source_system'])
|
||||
? substr(trim((string) $body['source_system']), 0, 120) : null;
|
||||
|
||||
// Dedup su external_id (org + ref)
|
||||
$externalId = isset($body['external_id']) ? substr(trim((string) $body['external_id']), 0, 190) : '';
|
||||
if ($externalId !== '') {
|
||||
$existing = Database::fetchOne(
|
||||
'SELECT id, incident_code FROM incidents WHERE organization_id = ? AND external_ref = ?',
|
||||
[$orgId, $externalId]
|
||||
);
|
||||
if ($existing) {
|
||||
$this->jsonSuccess([
|
||||
'id' => (int) $existing['id'],
|
||||
'incident_code' => $existing['incident_code'],
|
||||
'deduplicated' => true,
|
||||
], 'Alert già ingerito (dedup su external_id)', 200);
|
||||
return;
|
||||
}
|
||||
}
|
||||
$externalRef = $externalId !== '' ? $externalId : null;
|
||||
|
||||
$description = trim((string) ($body['description'] ?? ''));
|
||||
$ts = strtotime((string) ($body['detected_at'] ?? 'now'));
|
||||
$detectedAt = date('Y-m-d H:i:s', $ts ?: time());
|
||||
|
||||
$validClass = ['cyber_attack', 'data_breach', 'system_failure', 'human_error', 'natural_disaster', 'supply_chain', 'other'];
|
||||
$validSev = ['low', 'medium', 'high', 'critical'];
|
||||
|
||||
$classification = in_array(($body['classification'] ?? ''), $validClass, true) ? $body['classification'] : null;
|
||||
$severity = $this->mapSeverity($body['severity'] ?? null);
|
||||
$isSignificant = array_key_exists('is_significant', $body) ? (bool) $body['is_significant'] : null;
|
||||
$isType = null;
|
||||
$aiUsed = false;
|
||||
$aiReason = null;
|
||||
$org = null;
|
||||
|
||||
// Classificazione AI (best-effort) se mancano dati chiave
|
||||
$aiClassify = $body['ai_classify'] ?? true;
|
||||
if ($aiClassify && ($classification === null || $severity === null || $isSignificant === null)) {
|
||||
try {
|
||||
require_once __DIR__ . '/../services/AIService.php';
|
||||
$org = Database::fetchOne('SELECT sector, entity_type FROM organizations WHERE id = ?', [$orgId]);
|
||||
$ai = (new AIService())->classifyIncident($title, $description ?: $title, $org ?: []);
|
||||
if (is_array($ai)) {
|
||||
$aiUsed = true;
|
||||
if ($classification === null && in_array(($ai['classification'] ?? ''), $validClass, true)) {
|
||||
$classification = $ai['classification'];
|
||||
}
|
||||
if ($severity === null && in_array(($ai['severity'] ?? ''), $validSev, true)) {
|
||||
$severity = $ai['severity'];
|
||||
}
|
||||
if ($isSignificant === null && isset($ai['is_significant'])) {
|
||||
$isSignificant = (bool) $ai['is_significant'];
|
||||
}
|
||||
if (!empty($ai['nis2_incident_type']) && $ai['nis2_incident_type'] !== 'none') {
|
||||
$isType = $ai['nis2_incident_type'];
|
||||
}
|
||||
$aiReason = $ai['significance_reason'] ?? null;
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
error_log('[INGEST] AI classify error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Default sensati per alert da sistemi di sicurezza
|
||||
if ($classification === null) {
|
||||
$classification = in_array($source, ['siem', 'soc', 'edr'], true) ? 'cyber_attack' : 'other';
|
||||
}
|
||||
if ($severity === null) $severity = 'medium';
|
||||
if ($isSignificant === null) $isSignificant = false;
|
||||
|
||||
// Regime obblighi NIS2 (Determina ACN 164179/2025) — coerente con IncidentController::create
|
||||
if ($org === null) {
|
||||
$org = Database::fetchOne('SELECT entity_type FROM organizations WHERE id = ?', [$orgId]);
|
||||
}
|
||||
$entityObligation = ($org && ($org['entity_type'] ?? '') === 'essential') ? 'essential' : 'important';
|
||||
$validIs = $entityObligation === 'essential' ? ['IS-1', 'IS-2', 'IS-3', 'IS-4'] : ['IS-1', 'IS-2', 'IS-3'];
|
||||
if ($isType !== null && !in_array($isType, $validIs, true)) {
|
||||
$isType = null;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'organization_id' => $orgId,
|
||||
'incident_code' => $this->generateCode('INC'),
|
||||
'title' => $title,
|
||||
'description' => $description ?: null,
|
||||
'classification' => $classification,
|
||||
'source' => $source,
|
||||
'source_system' => $sourceSystem,
|
||||
'external_ref' => $externalRef,
|
||||
'severity' => $severity,
|
||||
'is_significant' => $isSignificant ? 1 : 0,
|
||||
'nis2_incident_type' => $isType,
|
||||
'entity_obligation' => $entityObligation,
|
||||
'detected_at' => $detectedAt,
|
||||
'affected_services' => $body['affected_services'] ?? null,
|
||||
'affected_users_count' => isset($body['affected_users_count']) ? (int) $body['affected_users_count'] : null,
|
||||
'cross_border_impact' => !empty($body['cross_border_impact']) ? 1 : 0,
|
||||
'malicious_action' => !empty($body['malicious_action']) ? 1 : (in_array($source, ['siem', 'soc', 'edr'], true) ? 1 : 0),
|
||||
'reported_by' => null,
|
||||
'assigned_to' => null,
|
||||
];
|
||||
|
||||
if ($isSignificant) {
|
||||
$t = strtotime($detectedAt);
|
||||
$data['early_warning_due'] = date('Y-m-d H:i:s', $t + 24 * 3600);
|
||||
$data['notification_due'] = date('Y-m-d H:i:s', $t + 72 * 3600);
|
||||
$data['final_report_due'] = date('Y-m-d H:i:s', $t + 30 * 86400);
|
||||
}
|
||||
|
||||
$incidentId = Database::insert('incidents', $data);
|
||||
|
||||
Database::insert('incident_timeline', [
|
||||
'incident_id' => $incidentId,
|
||||
'event_type' => 'detection',
|
||||
'description' => 'Incidente ingerito automaticamente da ' . strtoupper($source)
|
||||
. ($sourceSystem ? " ({$sourceSystem})" : '')
|
||||
. ($aiReason ? '. Classificazione AI: ' . $aiReason : ''),
|
||||
'created_by' => null,
|
||||
]);
|
||||
|
||||
// Dispatch webhook (best-effort)
|
||||
try {
|
||||
require_once __DIR__ . '/../services/WebhookService.php';
|
||||
$incident = array_merge($data, ['id' => $incidentId]);
|
||||
$wh = new WebhookService();
|
||||
$wh->dispatch($orgId, 'incident.created', WebhookService::incidentPayload($incident, 'created'));
|
||||
if ($isSignificant) {
|
||||
$wh->dispatch($orgId, 'incident.significant', WebhookService::incidentPayload($incident, 'significant'));
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
error_log('[INGEST][WEBHOOK] ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$this->jsonSuccess([
|
||||
'id' => $incidentId,
|
||||
'incident_code' => $data['incident_code'],
|
||||
'deduplicated' => false,
|
||||
'source' => $source,
|
||||
'classification' => $classification,
|
||||
'severity' => $severity,
|
||||
'is_significant' => $isSignificant,
|
||||
'ai_classified' => $aiUsed,
|
||||
'deadlines' => $isSignificant ? [
|
||||
'early_warning' => $data['early_warning_due'],
|
||||
'notification' => $data['notification_due'],
|
||||
'final_report' => $data['final_report_due'],
|
||||
] : null,
|
||||
], 'Incidente ingerito', 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizza la severity di un alert esterno → enum incidents.
|
||||
* Accetta stringhe (high/critical/P1/sev2/warning…) o numeri (CVSS 0-10, scala 1-5).
|
||||
*/
|
||||
private function mapSeverity($val): ?string
|
||||
{
|
||||
if ($val === null || $val === '') {
|
||||
return null;
|
||||
}
|
||||
if (is_numeric($val)) {
|
||||
$n = (float) $val;
|
||||
if ($n <= 5) { // scala 1-5
|
||||
return $n >= 4 ? 'critical' : ($n >= 3 ? 'high' : ($n >= 2 ? 'medium' : 'low'));
|
||||
}
|
||||
return $n >= 9 ? 'critical' : ($n >= 7 ? 'high' : ($n >= 4 ? 'medium' : 'low')); // CVSS 0-10
|
||||
}
|
||||
$s = strtolower(trim((string) $val));
|
||||
$map = [
|
||||
'critical' => 'critical', 'crit' => 'critical', 'sev1' => 'critical', 'p1' => 'critical', 'emergency' => 'critical', 'fatal' => 'critical',
|
||||
'high' => 'high', 'severe' => 'high', 'sev2' => 'high', 'p2' => 'high', 'error' => 'high',
|
||||
'medium' => 'medium', 'moderate' => 'medium', 'warning' => 'medium', 'warn' => 'medium', 'sev3' => 'medium', 'p3' => 'medium',
|
||||
'low' => 'low', 'info' => 'low', 'informational' => 'low', 'notice' => 'low', 'sev4' => 'low', 'p4' => 'low', 'p5' => 'low',
|
||||
];
|
||||
return $map[$s] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,11 +16,34 @@ class VectorService
|
||||
// PHP-FPM Alpine non popola correttamente env via getenv() (clear_env non
|
||||
// applicato + bug DNS musl per hostname senza dots). Workaround: leggi da
|
||||
// multiple sources e ricadi su IP statico del container Qdrant.
|
||||
// Risoluzione Qdrant URL. Bug noto (verificato 2026-05-29): nei worker PHP-FPM
|
||||
// (Alpine/musl) durante una request HTTP NON funziona né getenv('QDRANT_URL')
|
||||
// (ritorna false) né la risoluzione DNS dell'hostname (gethostbyname e curl
|
||||
// falliscono per i nomi single-label Docker). Funziona SOLO un IP letterale.
|
||||
// In CLI invece getenv + gethostbyname funzionano. Strategia:
|
||||
// - env (CLI): usa QDRANT_URL (hostname) e, sotto, gethostbyname -> IP (drift-proof in CLI)
|
||||
// - fpm: env=false -> fallback IP LETTERALE qui sotto (unico modo che funziona in fpm)
|
||||
// NOTA DRIFT: se il container nis2-qdrant viene ricreato e cambia IP, aggiornare
|
||||
// questo fallback. Fix definitivo: assegnare ipv4_address statico a qdrant in
|
||||
// docker-compose.yml (richiede recreate della rete) e allineare qui l'IP.
|
||||
$url = getenv('QDRANT_URL')
|
||||
?: ($_SERVER['QDRANT_URL'] ?? null)
|
||||
?: ($_ENV['QDRANT_URL'] ?? null)
|
||||
?: 'http://nis2-qdrant:6333'; // hostname Qdrant (agg. 2026-05-29): drift-proof. L'IP hardcoded .5 era driftato a .3; con clear_env=no php-fpm eredita QDRANT_URL e risolve l'hostname via Docker DNS (CLI verificato 200). Evita ricorrenze del drift IP.
|
||||
$this->qdrantUrl = rtrim($url, '/');
|
||||
?: 'http://172.21.0.3:6333'; // IP letterale nis2-qdrant (fpm-safe). Vedi nota drift sopra.
|
||||
// Se l'URL contiene un hostname (caso CLI/env), prova a risolverlo a IP per
|
||||
// evitare il problema di risoluzione dentro curl. In fpm gethostbyname fallisce
|
||||
// e l'URL resta invariato: per questo il fallback sopra è già un IP.
|
||||
$url = rtrim($url, '/');
|
||||
if (preg_match('#^(https?://)([^/:]+)(:\d+)?(.*)$#', $url, $m)) {
|
||||
$host = $m[2];
|
||||
if (!filter_var($host, FILTER_VALIDATE_IP)) {
|
||||
$ip = gethostbyname($host);
|
||||
if ($ip && $ip !== $host && filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
$url = $m[1] . $ip . ($m[3] ?? '') . ($m[4] ?? '');
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->qdrantUrl = $url;
|
||||
$this->collection = $collection;
|
||||
}
|
||||
|
||||
|
||||
@ -34,6 +34,31 @@ Scoperto che **"Agile" = `consulting_firms` id 1 → "Agile Technology SRL"** (u
|
||||
- ❌ **La chiave SSH documentata `docs/credentials/hetzner_key` è ASSENTE.**
|
||||
- ✅ Usata la **chiave SSH effimera `.ssh-temp/id_ed25519_nis2-agile_8h_20260529-120834`** (validità 8h da 12:08 → scade ~20:08 del 29/05) per `ssh root@135.181.149.254` → `mysql -h localhost -u nis2_user`. Creds DB da `/var/www/nis2-agile/.env` sul server.
|
||||
|
||||
### 3. Integrazione analisi `docs/nis2/` → prodotto (v1.7.0) — DEPLOYATA in produzione
|
||||
L'utente ha caricato in `docs/nis2/` mockup HTML di un sistema NIS2 + 5 PDF normativi ufficiali. Integrati nel prodotto e **deployati live**. Doc completo: **`docs/nis2/INTEGRAZIONE_COMPLETATA.md`**.
|
||||
- **Fase 1** Asset Relevance Scoring NIS2 0-100 a 6 criteri (GV.OC-04): `AssetScoringService.php`, endpoint `assets/scoringGrid|{id}/score|relevantSystems`, UI `assets.html`, registro stampabile (`audit/relevantSystemsRegister`). SQL `020`.
|
||||
- **Fase 2** Tassonomia incidenti Determina ACN 164179/2025: IS-1..4 + `entity_obligation` essential/important (Allegati 3/4). SQL `021`, `IncidentController::create`, `AIService::classifyIncident`.
|
||||
- **Fase 3** PIR 5-Whys + metriche TTD/TTC/TTR + timestamp di fase auto. SQL `022` (tab `incident_pir`), endpoint `incidents/{id}/metrics|pir`.
|
||||
- **Fase 4** Mapping NIST CSF 2.0 (43 controlli) reference-only: `audit/nistCsfMapping` (no migrazione).
|
||||
- **FONTI CERTE** (richiesta utente: AI+help citano fonti certe): `application/config/nis2_sources.php` (registry), `AIService::authoritativeSourcesBlock()` iniettato nei prompt (vieta riferimenti inventati), citazioni in `help.js`, **ingest PDF normativi nella KB**: `scripts/ingest-nis2-sources.php` → **287 chunk in Qdrant `nis2_kb` scope SYSTEM** (retrieval verificato).
|
||||
- **Analisi concorrenza Evix**: `docs/EVIX_ANALISI_CONCORRENZA.html` (gap-driven, suite=prodotti Agile, vs GRC intl + NIS2 IT).
|
||||
|
||||
**Eseguito su Hetzner**: backup `/root/backup_pre_v170_20260529_165447.sql`; migrazioni 020/021/022 applicate; ingest 287 chunk; smoke test endpoint (401 ok).
|
||||
|
||||
### ⚙️ Scoperte infrastrutturali NON OVVIE (per prossime sessioni)
|
||||
- 🔑 **`/projects/nis2-agile` (devenv) e `/var/www/nis2-agile` (host) sono LO STESSO filesystem via bind mount** → le modifiche ai file sono **già live in produzione**, scp/git-pull NON servono. Confermato: stesso `git status`.
|
||||
- 🐛 **Qdrant IP drift**: `nis2-qdrant` non ha IP statico in compose, era driftato `172.21.0.5`→`172.21.0.3`. L'IP era hardcoded in `VectorService.php` E in `docker-compose.yml` (`QDRANT_URL`). **Fix applicato**: entrambi → hostname `http://nis2-qdrant:6333` (drift-proof). `app` ricreato (`docker compose up -d app`), vault/chiavi preservate, RAG web verificata.
|
||||
- ⚠️ **`docker.conf` ha `clear_env = no`** → php-fpm **EREDITA** le env (contraddice la vecchia nota CLAUDE.md su clear_env): per questo l'IP morto in `QDRANT_URL` rompeva la RAG web. CLI risolve l'hostname Qdrant via Docker DNS.
|
||||
- ℹ️ `pdftotext` presente sull'host ma NON nel container `nis2-app` (alpine); Qdrant raggiungibile solo dal container (network), non dall'host. Strategia ingest: estrarre testo su host in cache `.txt`, far girare l'ingest nel container (legge `.txt`).
|
||||
- ✅ **ARIA cablato**: creato `AiController::ask` (`POST /api/ai/ask`) → `AIService::askWithRag`. Il FAB ARIA (`common.js`) chiamava `/api/ai/ask` ma il controller NON esisteva → assistente AI **era rotto**, ora funziona e cita le fonti certe (verificato in prod: `rag_used=True`, sources = Ambiti/Determina). `kb_uploaded_documents` non esiste su questo DB (mig. KB 012-014 non applicate qui) → tracking MySQL KB saltato (best-effort), ma la ricerca legge da Qdrant.
|
||||
- 🐛🔑 **DNS php-fpm (verificato con diagnostico fpm-fcgi reale, poi rimosso)**: nei worker php-fpm Alpine/musl, durante una request HTTP, **`getenv('QDRANT_URL')` ritorna false** E **`gethostbyname`/curl NON risolvono gli hostname Docker single-label** (`nis2-qdrant` → "Could not resolve host"). **Funziona SOLO un IP letterale** (172.21.0.3 → 200). In CLI invece getenv+gethostbyname funzionano. Perciò: `VectorService` fallback = **IP letterale `172.21.0.3`** (fpm-safe, live via bind mount); `QDRANT_URL` compose = hostname (per CLI/cron, drift-proof via gethostbyname). ⚠️ **DRIFT**: se nis2-qdrant viene ricreato e cambia IP, aggiornare il fallback in `VectorService.php`. **Fix definitivo** (non fatto, richiede recreate rete): `ipv4_address` statico per qdrant in `docker-compose.yml` (subnet 172.21.0.0/16; IP liberi es. .10). NB: `clear_env=no` in docker.conf NON basta a passare l'env ai worker (verificato getenv=false).
|
||||
- ℹ️ Esiste un `public/_dbg_voyage.php` (debug Voyage, non mio) ancora presente in prod — valutare rimozione.
|
||||
|
||||
### ⚠️ TODO aperti (sessione 3)
|
||||
- **PUSH Gitea**: 2 commit miei (`5c545ea` feat + `94d7867` fix Qdrant) + i 5 dell'agente sono SOLO locali. Serve `git-login` (token) + `git push origin main`. Tentato, fallito per token assente.
|
||||
- **Lavoro "prossimo" da AgileHub**: l'utente l'ha menzionato ma **senza allegare contenuto/ID ticket** → da chiarire alla ripresa.
|
||||
- Opzionale: cablare `askWithRag` a endpoint web; valutare IP statico Qdrant in compose; applicare migrazioni KB `kb_uploaded_documents` se serve la lista doc in UI.
|
||||
|
||||
---
|
||||
|
||||
## SESSIONE MATTINA — 2026-05-29 (TRPG alignment)
|
||||
|
||||
27
docs/sql/023_incident_ingestion.sql
Normal file
27
docs/sql/023_incident_ingestion.sql
Normal file
@ -0,0 +1,27 @@
|
||||
-- ============================================================
|
||||
-- Migration 023: Ingestion incidenti SIEM/SOC/EDR (P1 gap competitivo)
|
||||
-- Aggiunge tracciamento sorgente + riferimento esterno (dedup) su incidents
|
||||
-- Idempotente: usa procedura per ADD COLUMN IF NOT EXISTS
|
||||
-- ============================================================
|
||||
|
||||
DELIMITER $$
|
||||
DROP PROCEDURE IF EXISTS add_col_023 $$
|
||||
CREATE PROCEDURE add_col_023()
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='incidents' AND COLUMN_NAME='source') THEN
|
||||
ALTER TABLE incidents ADD COLUMN source ENUM('manual','siem','soc','edr','api','email') NOT NULL DEFAULT 'manual' COMMENT 'Origine incidente (ingestion automatica vs manuale)' AFTER classification;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='incidents' AND COLUMN_NAME='source_system') THEN
|
||||
ALTER TABLE incidents ADD COLUMN source_system VARCHAR(120) NULL COMMENT 'Nome sistema sorgente (es. Splunk, Sentinel, CrowdStrike)' AFTER source;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='incidents' AND COLUMN_NAME='external_ref') THEN
|
||||
ALTER TABLE incidents ADD COLUMN external_ref VARCHAR(190) NULL COMMENT 'ID alert esterno (dedup ingestion)' AFTER source_system;
|
||||
END IF;
|
||||
-- Indice univoco per dedup ingestion (org + ref esterno). NULL ammessi multipli (incidenti manuali).
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='incidents' AND INDEX_NAME='uq_incident_external_ref') THEN
|
||||
ALTER TABLE incidents ADD UNIQUE KEY uq_incident_external_ref (organization_id, external_ref);
|
||||
END IF;
|
||||
END $$
|
||||
DELIMITER ;
|
||||
CALL add_col_023();
|
||||
DROP PROCEDURE IF EXISTS add_col_023;
|
||||
@ -108,6 +108,7 @@ $controllerMap = [
|
||||
'mktg-lead' => 'MktgLeadController', // standard condiviso TRPG/NIS2
|
||||
'feedback' => 'FeedbackController', // segnalazioni & risoluzione AI
|
||||
'knowledgebase' => 'KnowledgeBaseController', // KB multi-livello (Migration 012-014)
|
||||
'ai' => 'AiController', // Assistente AI ARIA (askWithRag + fonti certe)
|
||||
'branding' => 'BrandingController', // White-label firm (Fase 5 / G16)
|
||||
];
|
||||
|
||||
@ -347,6 +348,7 @@ $actionMap = [
|
||||
'GET:complianceSummary' => 'complianceSummary',
|
||||
'GET:risksFeed' => 'risksFeed',
|
||||
'GET:incidentsFeed' => 'incidentsFeed',
|
||||
'POST:incidentsIngest' => 'ingestIncident', // P1: ingestion alert SIEM/SOC/EDR
|
||||
'GET:controlsStatus' => 'controlsStatus',
|
||||
'GET:assetsCritical' => 'assetsCritical',
|
||||
'GET:suppliersRisk' => 'suppliersRisk',
|
||||
@ -433,6 +435,10 @@ $actionMap = [
|
||||
'DELETE:{id}' => 'delete',
|
||||
],
|
||||
|
||||
'ai' => [
|
||||
'POST:ask' => 'ask',
|
||||
],
|
||||
|
||||
// ── BrandingController — White-label firm (Fase 5 / G16) ──
|
||||
'branding' => [
|
||||
'GET:current' => 'getCurrent',
|
||||
|
||||
@ -39,6 +39,7 @@
|
||||
}
|
||||
.badge-available { background: rgba(34,197,94,.15); color: #22c55e; }
|
||||
.badge-planned { background: rgba(234,179,8,.15); color: #eab308; }
|
||||
.badge-roadmap { background: rgba(6,182,212,.15); color: #06b6d4; }
|
||||
.provider-card h3 { font-size: .95rem; margin-bottom: .4rem; }
|
||||
.provider-card p { font-size: .82rem; color: var(--text-secondary); line-height: 1.5; }
|
||||
|
||||
@ -251,6 +252,66 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Connettori inbound: Evidence Automation — colma il gap connettori (vs Vanta/Drata) emerso dall'analisi concorrenza Evix -->
|
||||
<div class="section-title">Catalogo Connettori — Evidence Automation
|
||||
<span style="font-size:.7rem;color:var(--text-secondary);font-weight:400">(Continuous Control Monitoring)</span>
|
||||
</div>
|
||||
<div class="integ-hero" style="margin-bottom:1rem">
|
||||
<p style="margin:0">Connettori <strong>inbound</strong> per la raccolta <strong>automatica</strong> delle evidenze di conformità
|
||||
(NIS2 Art.21) da cloud, identità ed endpoint — costruiti sulla base esistente <strong>Services API + Webhook HMAC</strong>.
|
||||
Riducono la raccolta manuale e abilitano il monitoraggio continuo dei controlli.</p>
|
||||
</div>
|
||||
<div class="provider-grid">
|
||||
<div class="provider-card">
|
||||
<span class="badge badge-roadmap">In roadmap</span>
|
||||
<h3>🟦 Microsoft 365 / Entra ID</h3>
|
||||
<p>Evidenze automatiche: MFA & conditional access, offboarding utenti, sicurezza mailbox, log di audit.
|
||||
Mappate su Art.21 (controllo accessi, gestione identità).</p>
|
||||
</div>
|
||||
<div class="provider-card">
|
||||
<span class="badge badge-roadmap">In roadmap</span>
|
||||
<h3>🟩 Google Workspace</h3>
|
||||
<p>Verifica 2SV, policy amministrative, gestione dispositivi e accessi.
|
||||
Evidenze per controllo accessi e sicurezza degli endpoint.</p>
|
||||
</div>
|
||||
<div class="provider-card">
|
||||
<span class="badge badge-roadmap">In roadmap</span>
|
||||
<h3>🟧 Amazon Web Services</h3>
|
||||
<p>IAM, CloudTrail, Security Hub, cifratura S3/EBS.
|
||||
Raccolta evidenze su crittografia, logging e gestione accessi cloud.</p>
|
||||
</div>
|
||||
<div class="provider-card">
|
||||
<span class="badge badge-roadmap">In roadmap</span>
|
||||
<h3>🟦 Microsoft Azure</h3>
|
||||
<p>Defender for Cloud, Azure Policy, Network Security Group.
|
||||
Posture di sicurezza cloud → controlli Art.21 e asset critici.</p>
|
||||
</div>
|
||||
<div class="provider-card">
|
||||
<span class="badge badge-roadmap">In roadmap</span>
|
||||
<h3>🔐 Identity Provider (Okta / Entra / Google)</h3>
|
||||
<p>Enforcement SSO/MFA, lifecycle utenti, revoche accessi.
|
||||
Evidenza continua su gestione identità e accessi privilegiati.</p>
|
||||
</div>
|
||||
<div class="provider-card">
|
||||
<span class="badge badge-roadmap">In roadmap</span>
|
||||
<h3>🛡️ EDR / XDR (CrowdStrike, SentinelOne, Defender)</h3>
|
||||
<p>Copertura endpoint, rilevamento minacce e stato agenti →
|
||||
popolano gli asset critici e aprono incidenti Art.23 in automatico.</p>
|
||||
</div>
|
||||
<div class="provider-card">
|
||||
<span class="badge badge-roadmap">In roadmap</span>
|
||||
<h3>📥 SIEM / SOC — Ingestion (inbound)</h3>
|
||||
<p>Alert da Splunk, Microsoft Sentinel, Elastic, Wazuh → apertura automatica di incidenti
|
||||
NIS2 (Art.23) con triage e timeline. Complementare al webhook outbound già disponibile.</p>
|
||||
</div>
|
||||
<div class="provider-card">
|
||||
<span class="badge badge-roadmap">In roadmap</span>
|
||||
<h3>🎫 Ticketing (Jira / ServiceNow)</h3>
|
||||
<p>Sincronizzazione bidirezionale di non-conformità/CAPA e task di raccolta evidenze.
|
||||
Le azioni correttive NIS2 diventano ticket tracciati.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tab-nav">
|
||||
<button class="tab-btn active" onclick="showTab('api')">Services API</button>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user