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>
185 lines
6.7 KiB
PHP
185 lines
6.7 KiB
PHP
<?php
|
|
/**
|
|
* NIS2 Agile - Policy Controller
|
|
*
|
|
* Gestione policy e procedure di sicurezza, con AI generation.
|
|
*/
|
|
|
|
require_once __DIR__ . '/BaseController.php';
|
|
require_once APP_PATH . '/services/AIService.php';
|
|
require_once APP_PATH . '/services/WebhookService.php';
|
|
|
|
class PolicyController extends BaseController
|
|
{
|
|
public function list(): void
|
|
{
|
|
$this->requireOrgAccess();
|
|
|
|
$where = 'organization_id = ?';
|
|
$params = [$this->getCurrentOrgId()];
|
|
|
|
if ($this->hasParam('status')) {
|
|
$where .= ' AND status = ?';
|
|
$params[] = $this->getParam('status');
|
|
}
|
|
if ($this->hasParam('category')) {
|
|
$where .= ' AND category = ?';
|
|
$params[] = $this->getParam('category');
|
|
}
|
|
|
|
$policies = Database::fetchAll(
|
|
"SELECT p.*, u.full_name as approved_by_name
|
|
FROM policies p
|
|
LEFT JOIN users u ON u.id = p.approved_by
|
|
WHERE p.{$where}
|
|
ORDER BY p.category, p.title",
|
|
$params
|
|
);
|
|
|
|
$this->jsonSuccess($policies);
|
|
}
|
|
|
|
public function create(): void
|
|
{
|
|
$this->requireOrgRole(['org_admin', 'compliance_manager']);
|
|
$this->validateRequired(['title', 'category']);
|
|
|
|
$policyId = Database::insert('policies', [
|
|
'organization_id' => $this->getCurrentOrgId(),
|
|
'title' => trim($this->getParam('title')),
|
|
'category' => $this->getParam('category'),
|
|
'nis2_article' => $this->getParam('nis2_article'),
|
|
'content' => $this->getParam('content'),
|
|
'next_review_date' => $this->getParam('next_review_date'),
|
|
'ai_generated' => $this->getParam('ai_generated', 0),
|
|
]);
|
|
|
|
$this->logAudit('policy_created', 'policy', $policyId);
|
|
$this->jsonSuccess(['id' => $policyId], 'Policy creata', 201);
|
|
}
|
|
|
|
public function get(int $id): void
|
|
{
|
|
$this->requireOrgAccess();
|
|
|
|
$policy = Database::fetchOne(
|
|
'SELECT p.*, u.full_name as approved_by_name
|
|
FROM policies p
|
|
LEFT JOIN users u ON u.id = p.approved_by
|
|
WHERE p.id = ? AND p.organization_id = ?',
|
|
[$id, $this->getCurrentOrgId()]
|
|
);
|
|
|
|
if (!$policy) {
|
|
$this->jsonError('Policy non trovata', 404, 'POLICY_NOT_FOUND');
|
|
}
|
|
|
|
$this->jsonSuccess($policy);
|
|
}
|
|
|
|
public function update(int $id): void
|
|
{
|
|
$this->requireOrgRole(['org_admin', 'compliance_manager']);
|
|
|
|
$updates = [];
|
|
foreach (['title', 'content', 'category', 'nis2_article', 'status', 'version', 'next_review_date'] as $field) {
|
|
if ($this->hasParam($field)) {
|
|
$updates[$field] = $this->getParam($field);
|
|
}
|
|
}
|
|
|
|
if (!empty($updates)) {
|
|
Database::update('policies', $updates, 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]);
|
|
$this->logAudit('policy_updated', 'policy', $id, $updates);
|
|
}
|
|
|
|
$this->jsonSuccess($updates, 'Policy aggiornata');
|
|
}
|
|
|
|
public function delete(int $id): void
|
|
{
|
|
$this->requireOrgRole(['org_admin']);
|
|
|
|
$deleted = Database::delete('policies', 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]);
|
|
if ($deleted === 0) {
|
|
$this->jsonError('Policy non trovata', 404, 'POLICY_NOT_FOUND');
|
|
}
|
|
|
|
$this->logAudit('policy_deleted', 'policy', $id);
|
|
$this->jsonSuccess(null, 'Policy eliminata');
|
|
}
|
|
|
|
public function approve(int $id): void
|
|
{
|
|
$this->requireOrgRole(['org_admin']);
|
|
|
|
Database::update('policies', [
|
|
'status' => 'approved',
|
|
'approved_by' => $this->getCurrentUserId(),
|
|
'approved_at' => date('Y-m-d H:i:s'),
|
|
], 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]);
|
|
|
|
$this->logAudit('policy_approved', 'policy', $id);
|
|
|
|
// Dispatch webhook policy.approved
|
|
try {
|
|
$policy = Database::fetchOne('SELECT * FROM policies WHERE id = ?', [$id]);
|
|
(new WebhookService())->dispatch(
|
|
$this->getCurrentOrgId(),
|
|
'policy.approved',
|
|
WebhookService::policyPayload($policy)
|
|
);
|
|
} catch (Throwable $e) {
|
|
error_log('[WEBHOOK] dispatch error: ' . $e->getMessage());
|
|
}
|
|
|
|
$this->jsonSuccess(null, 'Policy approvata');
|
|
}
|
|
|
|
public function aiGeneratePolicy(): void
|
|
{
|
|
$this->requireOrgRole(['org_admin', 'compliance_manager']);
|
|
$this->validateRequired(['category']);
|
|
|
|
$category = $this->getParam('category');
|
|
$org = Database::fetchOne('SELECT * FROM organizations WHERE id = ?', [$this->getCurrentOrgId()]);
|
|
|
|
try {
|
|
$aiService = new AIService();
|
|
$generated = $aiService->generatePolicy($category, $org);
|
|
|
|
$aiService->logInteraction(
|
|
$this->getCurrentOrgId(),
|
|
$this->getCurrentUserId(),
|
|
'policy_draft',
|
|
"Generate {$category} policy",
|
|
substr($generated['title'] ?? '', 0, 500)
|
|
);
|
|
|
|
$this->jsonSuccess($generated, 'Policy generata dall\'AI');
|
|
} catch (Throwable $e) {
|
|
$this->jsonError('Errore AI: ' . $e->getMessage(), 500, 'AI_ERROR');
|
|
}
|
|
}
|
|
|
|
public function getTemplates(): void
|
|
{
|
|
$this->requireOrgAccess();
|
|
|
|
$templates = [
|
|
['category' => 'information_security', 'title' => 'Politica di Sicurezza delle Informazioni', 'nis2_article' => '21.2.a'],
|
|
['category' => 'access_control', 'title' => 'Politica di Controllo degli Accessi', 'nis2_article' => '21.2.i'],
|
|
['category' => 'incident_response', 'title' => 'Piano di Risposta agli Incidenti', 'nis2_article' => '21.2.b'],
|
|
['category' => 'business_continuity', 'title' => 'Piano di Continuità Operativa', 'nis2_article' => '21.2.c'],
|
|
['category' => 'supply_chain', 'title' => 'Politica di Sicurezza della Supply Chain', 'nis2_article' => '21.2.d'],
|
|
['category' => 'encryption', 'title' => 'Politica sulla Crittografia', 'nis2_article' => '21.2.h'],
|
|
['category' => 'hr_security', 'title' => 'Politica di Sicurezza delle Risorse Umane', 'nis2_article' => '21.2.i'],
|
|
['category' => 'asset_management', 'title' => 'Politica di Gestione degli Asset', 'nis2_article' => '21.2.i'],
|
|
['category' => 'network_security', 'title' => 'Politica di Sicurezza della Rete', 'nis2_article' => '21.2.e'],
|
|
['category' => 'vulnerability_management', 'title' => 'Politica di Gestione delle Vulnerabilità', 'nis2_article' => '21.2.e'],
|
|
];
|
|
|
|
$this->jsonSuccess($templates);
|
|
}
|
|
}
|