nis2-agile/application/controllers/DashboardController.php
Cristiano Benassati ae78a2f7f4 [CORE] Initial project scaffold - NIS2 Agile Compliance Platform
Complete MVP implementation including:
- PHP 8.4 backend with Front Controller pattern (80+ API endpoints)
- Multi-tenant architecture with organization_id isolation
- JWT authentication (HS256, 2h access + 7d refresh tokens)
- 14 controllers: Auth, Organization, Assessment, Dashboard, Risk,
  Incident, Policy, SupplyChain, Training, Asset, Audit, Admin
- AI Service integration (Anthropic Claude API) for gap analysis,
  risk suggestions, policy generation, incident classification
- NIS2 gap analysis questionnaire (~80 questions, 10 categories)
- MySQL schema (20 tables) with NIS2 Art. 21 compliance controls
- NIS2 Art. 23 incident reporting workflow (24h/72h/30d)
- Frontend: login, register, dashboard, assessment wizard, org setup
- Docker configuration (PHP-FPM + Nginx + MySQL)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 17:50:18 +01:00

349 lines
12 KiB
PHP

<?php
/**
* NIS2 Agile - Dashboard Controller
*
* Overview di compliance, score, scadenze, attività recenti.
*/
require_once __DIR__ . '/BaseController.php';
class DashboardController extends BaseController
{
/**
* GET /api/dashboard/overview
*/
public function overview(): void
{
$this->requireOrgAccess();
$orgId = $this->getCurrentOrgId();
// Ultimo assessment
$lastAssessment = Database::fetchOne(
'SELECT id, title, overall_score, status, completed_at
FROM assessments
WHERE organization_id = ? AND status = "completed"
ORDER BY completed_at DESC LIMIT 1',
[$orgId]
);
// Compliance controls status
$controlStats = Database::fetchAll(
'SELECT status, COUNT(*) as count
FROM compliance_controls
WHERE organization_id = ?
GROUP BY status',
[$orgId]
);
// Rischi aperti
$openRisks = Database::fetchAll(
'SELECT severity, COUNT(*) as count
FROM risks
WHERE organization_id = ? AND status NOT IN ("closed")
GROUP BY FIELD(severity, "critical", "high", "medium", "low") -- non supportato, usiamo ORDER',
[$orgId]
);
$riskCounts = Database::fetchOne(
'SELECT
SUM(CASE WHEN inherent_risk_score >= 20 THEN 1 ELSE 0 END) as critical_high,
SUM(CASE WHEN inherent_risk_score BETWEEN 10 AND 19 THEN 1 ELSE 0 END) as medium,
SUM(CASE WHEN inherent_risk_score < 10 THEN 1 ELSE 0 END) as low,
COUNT(*) as total
FROM risks
WHERE organization_id = ? AND status != "closed"',
[$orgId]
);
// Incidenti attivi
$activeIncidents = Database::count(
'incidents',
'organization_id = ? AND status NOT IN ("closed", "post_mortem")',
[$orgId]
);
// Policy per stato
$policyStats = Database::fetchAll(
'SELECT status, COUNT(*) as count
FROM policies
WHERE organization_id = ?
GROUP BY status',
[$orgId]
);
// Fornitori critici
$criticalSuppliers = Database::count(
'suppliers',
'organization_id = ? AND criticality IN ("high", "critical") AND security_requirements_met = 0',
[$orgId]
);
// Training completamento
$trainingStats = Database::fetchOne(
'SELECT
COUNT(*) as total,
SUM(CASE WHEN status = "completed" THEN 1 ELSE 0 END) as completed,
SUM(CASE WHEN status = "overdue" THEN 1 ELSE 0 END) as overdue
FROM training_assignments
WHERE organization_id = ?',
[$orgId]
);
// Conteggi asset
$assetCount = Database::count('assets', 'organization_id = ? AND status = "active"', [$orgId]);
// Organizzazione info
$org = Database::fetchOne('SELECT name, sector, entity_type, subscription_plan FROM organizations WHERE id = ?', [$orgId]);
$this->jsonSuccess([
'organization' => $org,
'last_assessment' => $lastAssessment,
'compliance_score' => $lastAssessment ? (float) $lastAssessment['overall_score'] : null,
'controls' => $controlStats,
'risks' => $riskCounts,
'active_incidents' => $activeIncidents,
'policies' => $policyStats,
'critical_suppliers' => $criticalSuppliers,
'training' => $trainingStats,
'asset_count' => $assetCount,
]);
}
/**
* GET /api/dashboard/compliance-score
*/
public function complianceScore(): void
{
$this->requireOrgAccess();
$orgId = $this->getCurrentOrgId();
// Score dall'ultimo assessment
$assessments = Database::fetchAll(
'SELECT id, title, overall_score, category_scores, completed_at
FROM assessments
WHERE organization_id = ? AND status = "completed"
ORDER BY completed_at DESC
LIMIT 5',
[$orgId]
);
// Score dei controlli
$controls = Database::fetchAll(
'SELECT control_code, title, status, implementation_percentage
FROM compliance_controls
WHERE organization_id = ?
ORDER BY control_code',
[$orgId]
);
$totalControls = count($controls);
$implementedControls = 0;
$avgImplementation = 0;
foreach ($controls as $c) {
if ($c['status'] === 'implemented' || $c['status'] === 'verified') {
$implementedControls++;
}
$avgImplementation += (int) $c['implementation_percentage'];
}
$avgImplementation = $totalControls > 0 ? round($avgImplementation / $totalControls) : 0;
$this->jsonSuccess([
'assessments' => $assessments,
'controls' => $controls,
'total_controls' => $totalControls,
'implemented_controls' => $implementedControls,
'avg_implementation' => $avgImplementation,
]);
}
/**
* GET /api/dashboard/upcoming-deadlines
*/
public function deadlines(): void
{
$this->requireOrgAccess();
$orgId = $this->getCurrentOrgId();
$deadlines = [];
// Incidenti con scadenze notifica
$incidentDeadlines = Database::fetchAll(
'SELECT id, incident_code, title, severity,
early_warning_due, early_warning_sent_at,
notification_due, notification_sent_at,
final_report_due, final_report_sent_at
FROM incidents
WHERE organization_id = ? AND is_significant = 1 AND status NOT IN ("closed", "post_mortem")
ORDER BY detected_at DESC',
[$orgId]
);
foreach ($incidentDeadlines as $inc) {
if ($inc['early_warning_due'] && !$inc['early_warning_sent_at']) {
$deadlines[] = [
'type' => 'incident_early_warning',
'title' => "Early Warning: {$inc['title']}",
'due_date' => $inc['early_warning_due'],
'severity' => 'critical',
'entity_type' => 'incident',
'entity_id' => $inc['id'],
];
}
if ($inc['notification_due'] && !$inc['notification_sent_at']) {
$deadlines[] = [
'type' => 'incident_notification',
'title' => "Notifica CSIRT: {$inc['title']}",
'due_date' => $inc['notification_due'],
'severity' => 'high',
'entity_type' => 'incident',
'entity_id' => $inc['id'],
];
}
if ($inc['final_report_due'] && !$inc['final_report_sent_at']) {
$deadlines[] = [
'type' => 'incident_final_report',
'title' => "Report finale: {$inc['title']}",
'due_date' => $inc['final_report_due'],
'severity' => 'medium',
'entity_type' => 'incident',
'entity_id' => $inc['id'],
];
}
}
// Policy in scadenza revisione
$policyDeadlines = Database::fetchAll(
'SELECT id, title, next_review_date
FROM policies
WHERE organization_id = ? AND next_review_date IS NOT NULL
AND next_review_date <= DATE_ADD(NOW(), INTERVAL 30 DAY)
AND status NOT IN ("archived")
ORDER BY next_review_date',
[$orgId]
);
foreach ($policyDeadlines as $p) {
$deadlines[] = [
'type' => 'policy_review',
'title' => "Revisione policy: {$p['title']}",
'due_date' => $p['next_review_date'],
'severity' => 'medium',
'entity_type' => 'policy',
'entity_id' => $p['id'],
];
}
// Risk treatments in scadenza
$treatmentDeadlines = Database::fetchAll(
'SELECT rt.id, rt.action_description, rt.due_date, r.title as risk_title
FROM risk_treatments rt
JOIN risks r ON r.id = rt.risk_id
WHERE r.organization_id = ? AND rt.status IN ("planned", "in_progress")
AND rt.due_date IS NOT NULL AND rt.due_date <= DATE_ADD(NOW(), INTERVAL 30 DAY)
ORDER BY rt.due_date',
[$orgId]
);
foreach ($treatmentDeadlines as $t) {
$deadlines[] = [
'type' => 'risk_treatment',
'title' => "Trattamento rischio: {$t['risk_title']}",
'due_date' => $t['due_date'],
'severity' => 'medium',
'entity_type' => 'risk_treatment',
'entity_id' => $t['id'],
];
}
// Training in scadenza
$trainingDeadlines = Database::fetchAll(
'SELECT ta.id, tc.title, ta.due_date, u.full_name
FROM training_assignments ta
JOIN training_courses tc ON tc.id = ta.course_id
JOIN users u ON u.id = ta.user_id
WHERE ta.organization_id = ? AND ta.status IN ("assigned", "in_progress")
AND ta.due_date IS NOT NULL AND ta.due_date <= DATE_ADD(NOW(), INTERVAL 30 DAY)
ORDER BY ta.due_date',
[$orgId]
);
foreach ($trainingDeadlines as $t) {
$deadlines[] = [
'type' => 'training_due',
'title' => "Formazione: {$t['title']} - {$t['full_name']}",
'due_date' => $t['due_date'],
'severity' => 'low',
'entity_type' => 'training',
'entity_id' => $t['id'],
];
}
// Ordina per data
usort($deadlines, fn($a, $b) => strcmp($a['due_date'], $b['due_date']));
$this->jsonSuccess($deadlines);
}
/**
* GET /api/dashboard/recent-activity
*/
public function recentActivity(): void
{
$this->requireOrgAccess();
$activities = Database::fetchAll(
'SELECT al.*, u.full_name
FROM audit_logs al
LEFT JOIN users u ON u.id = al.user_id
WHERE al.organization_id = ?
ORDER BY al.created_at DESC
LIMIT 20',
[$this->getCurrentOrgId()]
);
$this->jsonSuccess($activities);
}
/**
* GET /api/dashboard/risk-heatmap
*/
public function riskHeatmap(): void
{
$this->requireOrgAccess();
$risks = Database::fetchAll(
'SELECT id, title, category, likelihood, impact, inherent_risk_score, status
FROM risks
WHERE organization_id = ? AND status != "closed"
ORDER BY inherent_risk_score DESC',
[$this->getCurrentOrgId()]
);
// Costruisci matrice 5x5
$matrix = [];
for ($l = 1; $l <= 5; $l++) {
for ($i = 1; $i <= 5; $i++) {
$matrix["{$l}_{$i}"] = [];
}
}
foreach ($risks as $risk) {
if ($risk['likelihood'] && $risk['impact']) {
$key = "{$risk['likelihood']}_{$risk['impact']}";
$matrix[$key][] = [
'id' => $risk['id'],
'title' => $risk['title'],
'score' => $risk['inherent_risk_score'],
];
}
}
$this->jsonSuccess([
'risks' => $risks,
'matrix' => $matrix,
]);
}
}