nis2-agile/application/controllers/SupplyChainController.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

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;
}
}