[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>
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user