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>
169 lines
6.0 KiB
PHP
169 lines
6.0 KiB
PHP
<?php
|
|
/**
|
|
* NIS2 Agile - Supply Chain Controller
|
|
*
|
|
* Gestione fornitori, assessment cybersecurity, risk scoring.
|
|
*/
|
|
|
|
require_once __DIR__ . '/BaseController.php';
|
|
|
|
class SupplyChainController extends BaseController
|
|
{
|
|
public function list(): void
|
|
{
|
|
$this->requireOrgAccess();
|
|
|
|
$suppliers = Database::fetchAll(
|
|
'SELECT * FROM suppliers WHERE organization_id = ? ORDER BY criticality DESC, name',
|
|
[$this->getCurrentOrgId()]
|
|
);
|
|
|
|
$this->jsonSuccess($suppliers);
|
|
}
|
|
|
|
public function create(): void
|
|
{
|
|
$this->requireOrgRole(['org_admin', 'compliance_manager']);
|
|
$this->validateRequired(['name', 'service_type']);
|
|
|
|
$supplierId = Database::insert('suppliers', [
|
|
'organization_id' => $this->getCurrentOrgId(),
|
|
'name' => trim($this->getParam('name')),
|
|
'vat_number' => $this->getParam('vat_number'),
|
|
'contact_email' => $this->getParam('contact_email'),
|
|
'contact_name' => $this->getParam('contact_name'),
|
|
'service_type' => $this->getParam('service_type'),
|
|
'service_description' => $this->getParam('service_description'),
|
|
'criticality' => $this->getParam('criticality', 'medium'),
|
|
'contract_start_date' => $this->getParam('contract_start_date'),
|
|
'contract_expiry_date' => $this->getParam('contract_expiry_date'),
|
|
'notes' => $this->getParam('notes'),
|
|
]);
|
|
|
|
$this->logAudit('supplier_created', 'supplier', $supplierId);
|
|
$this->jsonSuccess(['id' => $supplierId], 'Fornitore aggiunto', 201);
|
|
}
|
|
|
|
public function get(int $id): void
|
|
{
|
|
$this->requireOrgAccess();
|
|
|
|
$supplier = Database::fetchOne(
|
|
'SELECT * FROM suppliers WHERE id = ? AND organization_id = ?',
|
|
[$id, $this->getCurrentOrgId()]
|
|
);
|
|
|
|
if (!$supplier) {
|
|
$this->jsonError('Fornitore non trovato', 404, 'SUPPLIER_NOT_FOUND');
|
|
}
|
|
|
|
$this->jsonSuccess($supplier);
|
|
}
|
|
|
|
public function update(int $id): void
|
|
{
|
|
$this->requireOrgRole(['org_admin', 'compliance_manager']);
|
|
|
|
$updates = [];
|
|
$fields = ['name', 'vat_number', 'contact_email', 'contact_name', 'service_type',
|
|
'service_description', 'criticality', 'contract_start_date', 'contract_expiry_date',
|
|
'security_requirements_met', 'notes', 'status'];
|
|
|
|
foreach ($fields as $field) {
|
|
if ($this->hasParam($field)) {
|
|
$updates[$field] = $this->getParam($field);
|
|
}
|
|
}
|
|
|
|
if (!empty($updates)) {
|
|
Database::update('suppliers', $updates, 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]);
|
|
$this->logAudit('supplier_updated', 'supplier', $id, $updates);
|
|
}
|
|
|
|
$this->jsonSuccess($updates, 'Fornitore aggiornato');
|
|
}
|
|
|
|
public function delete(int $id): void
|
|
{
|
|
$this->requireOrgRole(['org_admin']);
|
|
$deleted = Database::delete('suppliers', 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]);
|
|
if ($deleted === 0) {
|
|
$this->jsonError('Fornitore non trovato', 404, 'SUPPLIER_NOT_FOUND');
|
|
}
|
|
$this->logAudit('supplier_deleted', 'supplier', $id);
|
|
$this->jsonSuccess(null, 'Fornitore eliminato');
|
|
}
|
|
|
|
public function assessSupplier(int $id): void
|
|
{
|
|
$this->requireOrgRole(['org_admin', 'compliance_manager']);
|
|
$this->validateRequired(['assessment_responses']);
|
|
|
|
$responses = $this->getParam('assessment_responses');
|
|
$riskScore = $this->calculateSupplierRiskScore($responses);
|
|
|
|
Database::update('suppliers', [
|
|
'assessment_responses' => json_encode($responses),
|
|
'risk_score' => $riskScore,
|
|
'last_assessment_date' => date('Y-m-d'),
|
|
'next_assessment_date' => date('Y-m-d', strtotime('+6 months')),
|
|
'security_requirements_met' => $riskScore >= 70 ? 1 : 0,
|
|
], 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]);
|
|
|
|
$this->logAudit('supplier_assessed', 'supplier', $id, ['risk_score' => $riskScore]);
|
|
$this->jsonSuccess(['risk_score' => $riskScore], 'Assessment fornitore completato');
|
|
}
|
|
|
|
public function riskOverview(): void
|
|
{
|
|
$this->requireOrgAccess();
|
|
|
|
$overview = Database::fetchAll(
|
|
'SELECT criticality, status,
|
|
COUNT(*) as count,
|
|
AVG(risk_score) as avg_risk_score,
|
|
SUM(CASE WHEN security_requirements_met = 0 THEN 1 ELSE 0 END) as non_compliant
|
|
FROM suppliers
|
|
WHERE organization_id = ?
|
|
GROUP BY criticality, status',
|
|
[$this->getCurrentOrgId()]
|
|
);
|
|
|
|
$expiring = Database::fetchAll(
|
|
'SELECT id, name, contract_expiry_date, criticality
|
|
FROM suppliers
|
|
WHERE organization_id = ? AND contract_expiry_date IS NOT NULL
|
|
AND contract_expiry_date <= DATE_ADD(NOW(), INTERVAL 90 DAY)
|
|
AND status = "active"
|
|
ORDER BY contract_expiry_date',
|
|
[$this->getCurrentOrgId()]
|
|
);
|
|
|
|
$this->jsonSuccess([
|
|
'overview' => $overview,
|
|
'expiring_contracts' => $expiring,
|
|
]);
|
|
}
|
|
|
|
private function calculateSupplierRiskScore(array $responses): int
|
|
{
|
|
if (empty($responses)) return 0;
|
|
|
|
$totalScore = 0;
|
|
$totalWeight = 0;
|
|
|
|
foreach ($responses as $resp) {
|
|
$weight = $resp['weight'] ?? 1;
|
|
$value = match ($resp['value'] ?? '') {
|
|
'yes', 'implemented' => 100,
|
|
'partial' => 50,
|
|
default => 0,
|
|
};
|
|
$totalScore += $value * $weight;
|
|
$totalWeight += 100 * $weight;
|
|
}
|
|
|
|
return $totalWeight > 0 ? (int) round($totalScore / $totalWeight * 100) : 0;
|
|
}
|
|
}
|