[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:
DevEnv nis2-agile 2026-02-20 12:01:33 +01:00
parent 0e78ec24c1
commit 782389849f
9 changed files with 282 additions and 39 deletions

View File

@ -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,

View File

@ -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
*/

View File

@ -0,0 +1,85 @@
-- ============================================================
-- NIS2 Agile - Migration 006: Security & Performance Improvements
-- Data: 2026-02-20
-- Eseguire come: mysql -u root -p nis2_agile_db < 006_security_improvements.sql
-- ============================================================
-- ── 1. Indici performance su incidents ────────────────────────────────────
-- Per query filtrate per org + scadenze NIS2
ALTER TABLE incidents
ADD INDEX IF NOT EXISTS idx_inc_org_status (organization_id, status),
ADD INDEX IF NOT EXISTS idx_inc_org_significant (organization_id, is_significant),
ADD INDEX IF NOT EXISTS idx_inc_early_warning_due (organization_id, early_warning_due),
ADD INDEX IF NOT EXISTS idx_inc_notification_due (organization_id, notification_due),
ADD INDEX IF NOT EXISTS idx_inc_final_report_due (organization_id, final_report_due);
-- ── 2. Indici performance su risks ────────────────────────────────────────
ALTER TABLE risks
ADD INDEX IF NOT EXISTS idx_risks_org_status (organization_id, status),
ADD INDEX IF NOT EXISTS idx_risks_score (organization_id, inherent_risk_score DESC);
-- ── 3. Indici performance su audit_logs (immutabilità) ────────────────────
-- L'audit log deve essere append-only e ricercabile rapidamente
ALTER TABLE audit_logs
ADD INDEX IF NOT EXISTS idx_audit_org_created (organization_id, created_at DESC),
ADD INDEX IF NOT EXISTS idx_audit_entity (entity_type, entity_id);
-- ── 4. Trigger per rendere audit_log immutabile ───────────────────────────
-- Blocca UPDATE e DELETE sull'audit log (solo INSERT consentito)
DROP TRIGGER IF EXISTS prevent_audit_log_update;
CREATE TRIGGER prevent_audit_log_update
BEFORE UPDATE ON audit_logs
FOR EACH ROW
BEGIN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'audit_logs is append-only: UPDATE not permitted';
END;
DROP TRIGGER IF EXISTS prevent_audit_log_delete;
CREATE TRIGGER prevent_audit_log_delete
BEFORE DELETE ON audit_logs
FOR EACH ROW
BEGIN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'audit_logs is append-only: DELETE not permitted';
END;
-- ── 5. Colonna deleted_at per soft delete su tabelle critiche ─────────────
-- Permette di "eliminare" record senza perdere traccia storica
-- Risks: soft delete
ALTER TABLE risks
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP NULL DEFAULT NULL AFTER updated_at;
ALTER TABLE risks
ADD INDEX IF NOT EXISTS idx_risks_deleted (organization_id, deleted_at);
-- Policies: soft delete
ALTER TABLE policies
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP NULL DEFAULT NULL AFTER updated_at;
ALTER TABLE policies
ADD INDEX IF NOT EXISTS idx_policies_deleted (organization_id, deleted_at);
-- Suppliers: soft delete
ALTER TABLE suppliers
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP NULL DEFAULT NULL AFTER updated_at;
-- ── 6. Indice su refresh_tokens per performance ───────────────────────────
ALTER TABLE refresh_tokens
ADD INDEX IF NOT EXISTS idx_refresh_user_expires (user_id, expires_at);
-- ── 7. Pulizia automatica refresh token scaduti ───────────────────────────
-- Evento schedulato (richiede event scheduler abilitato)
DROP EVENT IF EXISTS cleanup_expired_refresh_tokens;
CREATE EVENT IF NOT EXISTS cleanup_expired_refresh_tokens
ON SCHEDULE EVERY 6 HOUR
STARTS CURRENT_TIMESTAMP
DO
DELETE FROM refresh_tokens WHERE expires_at < NOW() - INTERVAL 1 DAY;
-- ── 8. Verifica ───────────────────────────────────────────────────────────
SELECT 'Migration 006 completed successfully' AS status;
-- Abilita event scheduler (da eseguire manualmente se necessario):
-- SET GLOBAL event_scheduler = ON;

View File

@ -44,6 +44,20 @@
<!-- Steps Indicator -->
<div class="steps" id="steps-indicator"></div>
<!-- Progress Bar -->
<div style="margin-bottom:16px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;">
<span style="font-size:0.78rem; font-weight:600; color:var(--gray-600);">Avanzamento risposta domande</span>
<span style="font-size:0.78rem; font-weight:700; color:var(--primary);" id="progress-pct">0%</span>
</div>
<div style="background:var(--gray-200); border-radius:99px; height:8px; overflow:hidden;">
<div id="progress-bar-fill" style="height:100%; background:var(--primary); border-radius:99px; transition:width 0.3s ease; width:0%;"></div>
</div>
<div style="font-size:0.7rem; color:var(--gray-400); margin-top:4px;">
<span id="progress-answered">0</span> di <span id="progress-total">0</span> domande con risposta
</div>
</div>
<!-- Question Card -->
<div class="card mb-24">
<div class="card-header">
@ -295,6 +309,22 @@
}
}
function updateProgressBar() {
const total = questions.length;
const answered = Object.values(responses).filter(r => r && r.answer).length;
const pct = total > 0 ? Math.round((answered / total) * 100) : 0;
const pctEl = document.getElementById('progress-pct');
const fillEl = document.getElementById('progress-bar-fill');
const answeredEl = document.getElementById('progress-answered');
const totalEl = document.getElementById('progress-total');
if (pctEl) pctEl.textContent = pct + '%';
if (fillEl) fillEl.style.width = pct + '%';
if (answeredEl) answeredEl.textContent = answered;
if (totalEl) totalEl.textContent = total;
}
function renderCurrentQuestion() {
if (categories.length === 0) return;
@ -365,6 +395,7 @@
`;
document.getElementById('question-area').innerHTML = html;
updateProgressBar();
// Navigation buttons
document.getElementById('btn-prev').disabled = (currentCategoryIdx === 0 && currentQuestionIdx === 0);

View File

@ -29,7 +29,8 @@
<div id="compliance-gauge">
<div class="spinner-lg" style="margin:40px auto;"></div>
</div>
<p class="text-muted mt-8" style="font-size:0.8rem;">Punteggio complessivo di conformita' NIS2</p>
<p class="text-muted mt-8" style="font-size:0.8rem;">Avanzamento implementazione misure Art.21 NIS2</p>
<p class="text-muted" style="font-size:0.7rem; margin-top:2px;">Misura l'implementazione tecnica, non la conformita' legale</p>
</div>
</div>

View File

@ -519,6 +519,37 @@
<label class="form-label">Data e ora rilevamento <span class="required">*</span></label>
<input type="datetime-local" class="form-input" id="inc-detected-at" value="${dtLocal}">
</div>
<!-- Decision tree Art.23 NIS2 - significativita' incidente -->
<div style="background:var(--gray-50); border:1px solid var(--gray-200); border-radius:var(--border-radius); padding:14px; margin-bottom:16px;">
<div style="font-size:0.8rem; font-weight:700; color:var(--gray-700); margin-bottom:10px; display:flex; align-items:center; gap:6px;">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16" style="color:var(--primary);"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/></svg>
Criteri Art. 23 NIS2 — Incidente Significativo
</div>
<p style="font-size:0.75rem; color:var(--gray-500); margin-bottom:10px;">Un incidente e' significativo se soddisfa almeno uno dei criteri seguenti (Considerando 101 NIS2 / D.Lgs. 138/2024 art. 25):</p>
<div style="display:grid; gap:6px;">
<label style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
<input type="checkbox" id="crit-users" onchange="evalSignificance()" style="accent-color:var(--primary); width:16px; height:16px;">
<span>Ha causato o puo' causare interruzione grave dei servizi per <strong>&ge;500 utenti</strong></span>
</label>
<label style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
<input type="checkbox" id="crit-duration" onchange="evalSignificance()" style="accent-color:var(--primary); width:16px; height:16px;">
<span>Durata dell'interruzione superiore a <strong>4 ore</strong></span>
</label>
<label style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
<input type="checkbox" id="crit-crossborder" onchange="evalSignificance()" style="accent-color:var(--primary); width:16px; height:16px;">
<span>Impatto su altri Stati Membri UE (impatto transfrontaliero)</span>
</label>
<label style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
<input type="checkbox" id="crit-attack" onchange="evalSignificance()" style="accent-color:var(--primary); width:16px; height:16px;">
<span>E' un attacco cyber o data breach con dati personali coinvolti</span>
</label>
<label style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
<input type="checkbox" id="crit-financial" onchange="evalSignificance()" style="accent-color:var(--primary); width:16px; height:16px;">
<span>Perdita finanziaria stimata superiore a <strong>€100.000</strong></span>
</label>
</div>
<div id="significance-result" style="margin-top:10px; padding:8px 12px; border-radius:6px; font-size:0.8rem; font-weight:600; display:none;"></div>
</div>
<div class="form-group">
<label class="form-check-inline">
<input type="checkbox" id="inc-significant">
@ -1142,6 +1173,38 @@
}
}
// ── Art.23 Significance Evaluator ────────────────────────────
function evalSignificance() {
const criteria = [
document.getElementById('crit-users'),
document.getElementById('crit-duration'),
document.getElementById('crit-crossborder'),
document.getElementById('crit-attack'),
document.getElementById('crit-financial'),
].filter(Boolean);
const checkedCount = criteria.filter(c => c.checked).length;
const resultDiv = document.getElementById('significance-result');
const sigCheckbox = document.getElementById('inc-significant');
if (!resultDiv) return;
resultDiv.style.display = 'block';
if (checkedCount >= 1) {
resultDiv.style.background = '#fef2f2';
resultDiv.style.border = '1px solid #fca5a5';
resultDiv.style.color = 'var(--danger)';
resultDiv.innerHTML = '&#9888; ' + checkedCount + ' criteri soddisfatti — l'incidente e' probabilmente <strong>SIGNIFICATIVO</strong>. Attivare le notifiche Art. 23 (24h/72h/30gg).';
if (sigCheckbox) sigCheckbox.checked = true;
} else {
resultDiv.style.background = '#f0fdf4';
resultDiv.style.border = '1px solid #bbf7d0';
resultDiv.style.color = 'var(--secondary)';
resultDiv.innerHTML = '&#10003; Nessun criterio soddisfatto — l'incidente appare <strong>NON significativo</strong> ai fini Art. 23. Verificare manualmente prima di confermare.';
if (sigCheckbox) sigCheckbox.checked = false;
}
}
// ── Navigation ───────────────────────────────────────────────
function backToList() {
document.getElementById('view-detail').classList.add('hidden');

View File

@ -19,9 +19,8 @@ $origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, CORS_ALLOWED_ORIGINS)) {
header("Access-Control-Allow-Origin: {$origin}");
} elseif (APP_DEBUG) {
header("Access-Control-Allow-Origin: *");
}
// NOTE: No wildcard CORS even in debug mode — use CORS_ALLOWED_ORIGINS in config
header("Access-Control-Allow-Methods: " . CORS_ALLOWED_METHODS);
header("Access-Control-Allow-Headers: " . CORS_ALLOWED_HEADERS);

View File

@ -519,6 +519,14 @@
<span class="badge ${badgeClass}">${escapeHtml(statusLabel)}</span>
${policy.ai_generated ? '<span class="badge badge-info" style="margin-left:6px;">AI Generata</span>' : ''}
</div>
${policy.ai_generated && (policy.status === 'draft' || policy.status === 'review') ? `
<div style="background:#fff7ed; border:1px solid #fed7aa; border-radius:var(--border-radius); padding:12px 16px; margin-top:12px; display:flex; gap:10px; align-items:flex-start;">
<svg viewBox="0 0 20 20" fill="#c2410c" width="18" height="18" style="flex-shrink:0; margin-top:2px;"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
<div>
<strong style="color:#c2410c; font-size:0.875rem;">Bozza AI — Revisione obbligatoria prima dell'approvazione</strong>
<p style="font-size:0.78rem; color:#92400e; margin:4px 0 0;">Questo documento e' stato generato automaticamente da un sistema AI. Prima di approvarlo, verificare che sia conforme alle procedure interne, al quadro normativo applicabile e alla realta' specifica dell'organizzazione. L'AI puo' produrre contenuti imprecisi o incompleti.</p>
</div>
</div>` : ''}
</div>
<div class="detail-header-actions">
${policy.status === 'draft' || policy.status === 'review' ? `

View File

@ -442,7 +442,25 @@
const TREATMENT_STATUS_LABELS = {
planned: 'Pianificato', in_progress: 'In Corso', completed: 'Completato', cancelled: 'Annullato'
};
// Scala Likelihood (probabilità annua)
const LIKELIHOOD_LABELS = ['', 'Molto Basso', 'Basso', 'Medio', 'Alto', 'Molto Alto'];
const LIKELIHOOD_DETAILS = [
'',
'1 - Remota: <1% probabilità annua (evento eccezionale)',
'2 - Improbabile: 1-10% (evento raro ma documentato nel settore)',
'3 - Possibile: 10-30% (può verificarsi nel corso dell'attività)',
'4 - Probabile: 30-70% (ci si aspetta che avvenga almeno una volta l'anno)',
'5 - Quasi Certa: >70% (si verificherà con elevata probabilità)',
];
// Scala Impact (danno economico / utenti impattati)
const IMPACT_DETAILS = [
'',
'1 - Minimo: <€10k danno, <100 utenti, servizio ripristinato in <1h',
'2 - Basso: €10k-50k, 100-500 utenti, ripristino <8h',
'3 - Significativo: €50k-500k, 500-5000 utenti, ripristino <24h',
'4 - Grave: €500k-10M, 5000-50000 utenti, ripristino <7 giorni',
'5 - Catastrofico: >€10M, >50000 utenti, servizi critici nazionali impattati',
];
// ── State ────────────────────────────────────────────────────
let currentView = 'table';
@ -878,22 +896,16 @@
<div class="form-row">
<div class="form-group">
<label class="form-label">Probabilita': <span id="likelihood-label">${LIKELIHOOD_LABELS[likelihood]}</span></label>
<label class="form-label">Probabilita' <span style="color:var(--gray-400); font-size:0.75rem; font-weight:400;">(frequenza stimata annua)</span></label>
<div class="range-value-display" id="likelihood-value">${likelihood}</div>
<input type="range" class="form-range" id="risk-likelihood" min="1" max="5" value="${likelihood}" oninput="updateRiskScore()">
<div class="form-range-labels">
<span>1 - Molto Basso</span>
<span>5 - Molto Alto</span>
</div>
<div style="font-size:0.75rem; color:var(--gray-500); margin-top:4px; padding:6px 8px; background:var(--gray-50); border-radius:4px;" id="likelihood-detail">${LIKELIHOOD_DETAILS[likelihood]}</div>
</div>
<div class="form-group">
<label class="form-label">Impatto: <span id="impact-label">${LIKELIHOOD_LABELS[impact]}</span></label>
<label class="form-label">Impatto <span style="color:var(--gray-400); font-size:0.75rem; font-weight:400;">(danno economico / utenti)</span></label>
<div class="range-value-display" id="impact-value">${impact}</div>
<input type="range" class="form-range" id="risk-impact" min="1" max="5" value="${impact}" oninput="updateRiskScore()">
<div class="form-range-labels">
<span>1 - Molto Basso</span>
<span>5 - Molto Alto</span>
</div>
<div style="font-size:0.75rem; color:var(--gray-500); margin-top:4px; padding:6px 8px; background:var(--gray-50); border-radius:4px;" id="impact-detail">${IMPACT_DETAILS[impact]}</div>
</div>
</div>
@ -934,8 +946,10 @@
document.getElementById('likelihood-value').textContent = l;
document.getElementById('impact-value').textContent = i;
document.getElementById('likelihood-label').textContent = LIKELIHOOD_LABELS[l];
document.getElementById('impact-label').textContent = LIKELIHOOD_LABELS[i];
const lDetail = document.getElementById('likelihood-detail');
const iDetail = document.getElementById('impact-detail');
if (lDetail) lDetail.textContent = LIKELIHOOD_DETAILS[l];
if (iDetail) iDetail.textContent = IMPACT_DETAILS[i];
document.getElementById('score-preview-value').textContent = score;
document.getElementById('score-preview-value').style.color = getScoreColorForRisk(score);
}