[SEC+UX] Hardening sicurezza + miglioramenti UX pre-audit
SICUREZZA: - index.php: rimosso CORS wildcard in debug mode (solo origini autorizzate) - AuthController: getClientIP() con X-Forwarded-For sicuro (proxy-aware) - AuthController: refresh token con SELECT FOR UPDATE in transazione atomica - AIService: anonimizzazione dati org nei prompt Anthropic API (no nome/fatturato) UX AUDIT-READY: - dashboard.html: gauge rinominato 'Avanzamento implementazione misure Art.21' - incidents.html: decision tree Art.23 con 5 criteri per 'Is Significant?' - policies.html: banner warning obbligatorio su bozze generate da AI - risks.html: tooltip dettagliati scala Likelihood/Impact (ISO 27005) - assessment.html: progress bar % completamento risposta domande DB: - migration 006: indici performance + audit_log immutabile (trigger) + soft delete Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
0e78ec24c1
commit
782389849f
@@ -10,15 +10,34 @@ require_once APP_PATH . '/services/RateLimitService.php';
|
||||
|
||||
class AuthController extends BaseController
|
||||
{
|
||||
/**
|
||||
* Restituisce l'IP reale del client, gestendo proxy/nginx.
|
||||
*/
|
||||
private function getClientIP(): string
|
||||
{
|
||||
$remoteAddr = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
// Fidati di X-Forwarded-For solo se la richiesta arriva da localhost (nginx proxy)
|
||||
if (in_array($remoteAddr, ['127.0.0.1', '::1', 'unknown'])
|
||||
&& !empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$xForwardedFor = $_SERVER['HTTP_X_FORWARDED_FOR'];
|
||||
$ips = array_map('trim', explode(',', $xForwardedFor));
|
||||
$firstIp = filter_var($ips[0], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
|
||||
if ($firstIp !== false) {
|
||||
return $firstIp;
|
||||
}
|
||||
}
|
||||
return $remoteAddr;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/auth/register
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
// Rate limiting
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
$ip = $this->getClientIP();
|
||||
RateLimitService::check("register:{$ip}", RATE_LIMIT_AUTH_REGISTER);
|
||||
RateLimitService::increment("register:{$ip}");
|
||||
RateLimitService::increment("register:{$ip}"); // $ip defined above via getClientIP()
|
||||
|
||||
$this->validateRequired(['email', 'password', 'full_name']);
|
||||
|
||||
@@ -87,7 +106,7 @@ class AuthController extends BaseController
|
||||
public function login(): void
|
||||
{
|
||||
// Rate limiting
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
$ip = $this->getClientIP();
|
||||
RateLimitService::check("login:{$ip}", RATE_LIMIT_AUTH_LOGIN);
|
||||
RateLimitService::increment("login:{$ip}");
|
||||
|
||||
@@ -167,24 +186,34 @@ class AuthController extends BaseController
|
||||
$refreshToken = $this->getParam('refresh_token');
|
||||
$hashedToken = hash('sha256', $refreshToken);
|
||||
|
||||
// Verifica refresh token
|
||||
$tokenRecord = Database::fetchOne(
|
||||
'SELECT * FROM refresh_tokens WHERE token = ? AND expires_at > NOW()',
|
||||
[$hashedToken]
|
||||
);
|
||||
// Transazione atomica per evitare race condition (double-spend del refresh token)
|
||||
Database::beginTransaction();
|
||||
try {
|
||||
// SELECT FOR UPDATE: blocca il record per tutta la transazione
|
||||
$tokenRecord = Database::fetchOne(
|
||||
'SELECT * FROM refresh_tokens WHERE token = ? AND expires_at > NOW() FOR UPDATE',
|
||||
[$hashedToken]
|
||||
);
|
||||
|
||||
if (!$tokenRecord) {
|
||||
$this->jsonError('Refresh token non valido o scaduto', 401, 'INVALID_REFRESH_TOKEN');
|
||||
if (!$tokenRecord) {
|
||||
Database::rollback();
|
||||
$this->jsonError('Refresh token non valido o scaduto', 401, 'INVALID_REFRESH_TOKEN');
|
||||
}
|
||||
|
||||
// Elimina vecchio token atomicamente
|
||||
Database::delete('refresh_tokens', 'id = ?', [$tokenRecord['id']]);
|
||||
|
||||
// Genera nuovi tokens
|
||||
$userId = (int) $tokenRecord['user_id'];
|
||||
$accessToken = $this->generateJWT($userId);
|
||||
$newRefreshToken = $this->generateRefreshToken($userId);
|
||||
|
||||
Database::commit();
|
||||
} catch (Throwable $e) {
|
||||
Database::rollback();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Elimina vecchio token
|
||||
Database::delete('refresh_tokens', 'id = ?', [$tokenRecord['id']]);
|
||||
|
||||
// Genera nuovi tokens
|
||||
$userId = (int) $tokenRecord['user_id'];
|
||||
$accessToken = $this->generateJWT($userId);
|
||||
$newRefreshToken = $this->generateRefreshToken($userId);
|
||||
|
||||
$this->jsonSuccess([
|
||||
'access_token' => $accessToken,
|
||||
'refresh_token' => $newRefreshToken,
|
||||
|
||||
@@ -35,17 +35,18 @@ class AIService
|
||||
{
|
||||
$responseSummary = $this->summarizeResponses($responses);
|
||||
|
||||
// Anonimizzazione: non inviare nome org né fatturato esatto ad API esterna
|
||||
$employeeRange = $this->employeeRange((int)($organization['employee_count'] ?? 0));
|
||||
|
||||
$prompt = <<<PROMPT
|
||||
Sei un esperto consulente di cybersecurity specializzato nella Direttiva NIS2 (EU 2022/2555) e nel D.Lgs. 138/2024 italiano.
|
||||
|
||||
Analizza i risultati della gap analysis per l'organizzazione seguente e fornisci raccomandazioni dettagliate.
|
||||
|
||||
## Organizzazione
|
||||
- Nome: {$organization['name']}
|
||||
## Organizzazione (dati anonimizzati)
|
||||
- Settore: {$organization['sector']}
|
||||
- Tipo entità NIS2: {$organization['entity_type']}
|
||||
- Dipendenti: {$organization['employee_count']}
|
||||
- Fatturato annuo: EUR {$organization['annual_turnover_eur']}
|
||||
- Dimensione: {$employeeRange}
|
||||
|
||||
## Risultati Assessment (Score: {$overallScore}%)
|
||||
|
||||
@@ -131,8 +132,7 @@ PROMPT;
|
||||
$prompt = <<<PROMPT
|
||||
Sei un esperto di information security policy writing. Genera una policy aziendale per la categoria "{$category}" conforme alla Direttiva NIS2.
|
||||
|
||||
## Organizzazione
|
||||
- Nome: {$organization['name']}
|
||||
## Organizzazione (dati anonimizzati)
|
||||
- Settore: {$organization['sector']}
|
||||
- Tipo entità NIS2: {$organization['entity_type']}
|
||||
|
||||
@@ -245,6 +245,19 @@ PROMPT;
|
||||
return $data['content'][0]['text'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converte numero dipendenti in range anonimizzato
|
||||
*/
|
||||
private function employeeRange(int $count): string
|
||||
{
|
||||
if ($count <= 0) return 'Non specificato';
|
||||
if ($count <= 10) return 'Micro impresa (1-10 dipendenti)';
|
||||
if ($count <= 50) return 'Piccola impresa (11-50 dipendenti)';
|
||||
if ($count <= 250) return 'Media impresa (51-250 dipendenti)';
|
||||
if ($count <= 1000) return 'Grande impresa (251-1000 dipendenti)';
|
||||
return 'Grande organizzazione (>1000 dipendenti)';
|
||||
}
|
||||
|
||||
/**
|
||||
* Riassume le risposte dell'assessment per il prompt AI
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user