[FEAT] Services API, Webhook, Whistleblowing, Normative + integrazioni

Sprint completo — prodotto presentation-ready:

Services API (read-only, API Key + scope):
- GET /api/services/status|compliance-summary|risks-feed|incidents-feed
- GET /api/services/controls-status|assets-critical|suppliers-risk|policies-approved
- GET /api/services/openapi (spec OpenAPI 3.0.3 JSON)

Webhook Outbound (Stripe-like HMAC-SHA256):
- CRUD api_keys + webhook_subscriptions (Settings → 2 nuovi tab)
- WebhookService: retry 3x backoff (0s/5min/30min), delivery log
- Trigger auto in IncidentController, RiskController, PolicyController
- Delivery log, test ping, processRetry

Nuovi moduli:
- WhistleblowingController (Art.32 NIS2): anonimato garantito, timeline, token tracking
- NormativeController: feed NIS2/ACN/DORA con ACK tracciato per audit

Frontend:
- whistleblowing.html: form submit anonimo/firmato + gestione CISO
- normative.html: feed con presa visione documentata + progress bar ACK
- public/docs/api.html: documentazione API dark theme (Swagger-like)
- settings.html: tab API Keys + tab Webhook
- integrations/: guide per lg231, SustainAI, AllRisk, SIEM (widget + codice)
- Sidebar: Segnalazioni + Normative aggiunte a common.js

DB: migration 007 (api_keys, webhook_subscriptions, webhook_deliveries),
    008 (whistleblowing_reports + timeline),
    009 (normative_updates + normative_ack + seed NIS2/ACN/DORA/ISO)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-03-07 13:20:24 +01:00
co-authored by Claude Sonnet 4.6
parent 3f4b451e2a
commit 86e9bdded2
22 changed files with 5080 additions and 10 deletions
@@ -8,6 +8,7 @@
require_once __DIR__ . '/BaseController.php';
require_once APP_PATH . '/services/AIService.php';
require_once APP_PATH . '/services/EmailService.php';
require_once APP_PATH . '/services/WebhookService.php';
class IncidentController extends BaseController
{
@@ -97,6 +98,18 @@ class IncidentController extends BaseController
'severity' => $data['severity'], 'is_significant' => $isSignificant
]);
// Dispatch webhook events
try {
$incident = array_merge($data, ['id' => $incidentId]);
$webhookSvc = new WebhookService();
$webhookSvc->dispatch($this->getCurrentOrgId(), 'incident.created', WebhookService::incidentPayload($incident, 'created'));
if ($isSignificant) {
$webhookSvc->dispatch($this->getCurrentOrgId(), 'incident.significant', WebhookService::incidentPayload($incident, 'significant'));
}
} catch (Throwable $e) {
error_log('[WEBHOOK] dispatch error: ' . $e->getMessage());
}
$this->jsonSuccess([
'id' => $incidentId,
'incident_code' => $data['incident_code'],
@@ -187,6 +200,18 @@ class IncidentController extends BaseController
if (!empty($updates)) {
Database::update('incidents', $updates, 'id = ?', [$id]);
$this->logAudit('incident_updated', 'incident', $id, $updates);
// Dispatch webhook: incident.updated e incident.significant se appena flaggato
try {
$updatedIncident = Database::fetchOne('SELECT * FROM incidents WHERE id = ?', [$id]);
$webhookSvc = new WebhookService();
$webhookSvc->dispatch($this->getCurrentOrgId(), 'incident.updated', WebhookService::incidentPayload($updatedIncident, 'updated'));
if (isset($updates['is_significant']) && $updates['is_significant'] && !$incident['is_significant']) {
$webhookSvc->dispatch($this->getCurrentOrgId(), 'incident.significant', WebhookService::incidentPayload($updatedIncident, 'significant'));
}
} catch (Throwable $e) {
error_log('[WEBHOOK] dispatch error: ' . $e->getMessage());
}
}
$this->jsonSuccess($updates, 'Incidente aggiornato');