Compare commits
No commits in common. "9d3f8936e1e242bee412f9bd81c4db6b717e87fe" and "2fd4b7ff26fa415e56fd9087e5975252a35002fe" have entirely different histories.
9d3f8936e1
...
2fd4b7ff26
@ -318,7 +318,7 @@ class AssetController extends BaseController
|
|||||||
|
|
||||||
Database::update('assets', [
|
Database::update('assets', [
|
||||||
'relevance_score' => $result['score'],
|
'relevance_score' => $result['score'],
|
||||||
'relevance_criteria' => json_encode($criteria, JSON_UNESCAPED_UNICODE),
|
'relevance_criteria' => json_encode($result['breakdown'], JSON_UNESCAPED_UNICODE),
|
||||||
'relevance_class' => $result['class'],
|
'relevance_class' => $result['class'],
|
||||||
'is_nis2_relevant' => $result['is_relevant'] ? 1 : 0,
|
'is_nis2_relevant' => $result['is_relevant'] ? 1 : 0,
|
||||||
'criticality' => $result['criticality'],
|
'criticality' => $result['criticality'],
|
||||||
|
|||||||
@ -123,24 +123,16 @@ class PolicyController extends BaseController
|
|||||||
|
|
||||||
$policy = Database::fetchOne('SELECT * FROM policies WHERE id = ?', [$id]);
|
$policy = Database::fetchOne('SELECT * FROM policies WHERE id = ?', [$id]);
|
||||||
|
|
||||||
// Snapshot versione per storico/diff/attestation (P3).
|
// Snapshot versione per storico/diff/attestation (P3)
|
||||||
// Idempotente sulla coppia (policy_id, version): la riapprovazione della
|
|
||||||
// stessa versione aggiorna lo snapshot invece di duplicarlo (UNIQUE uq_policy_version).
|
|
||||||
try {
|
try {
|
||||||
Database::query(
|
Database::insert('policy_versions', [
|
||||||
'INSERT INTO policy_versions (policy_id, organization_id, version, content, change_note, created_by)
|
'policy_id' => $id,
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
'organization_id' => $this->getCurrentOrgId(),
|
||||||
ON DUPLICATE KEY UPDATE content = VALUES(content), change_note = VALUES(change_note),
|
'version' => $policy['version'] ?? '1.0',
|
||||||
created_by = VALUES(created_by), created_at = NOW()',
|
'content' => $policy['content'] ?? null,
|
||||||
[
|
'change_note' => $this->getParam('change_note') ?: 'Approvazione',
|
||||||
$id,
|
'created_by' => $this->getCurrentUserId(),
|
||||||
$this->getCurrentOrgId(),
|
]);
|
||||||
$policy['version'] ?? '1.0',
|
|
||||||
$policy['content'] ?? null,
|
|
||||||
$this->getParam('change_note') ?: 'Approvazione',
|
|
||||||
$this->getCurrentUserId(),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
error_log('[POLICY_VER] ' . $e->getMessage());
|
error_log('[POLICY_VER] ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
@ -316,67 +308,18 @@ class PolicyController extends BaseController
|
|||||||
}
|
}
|
||||||
$linesA = preg_split('/\r\n|\r|\n/', (string) $a['content']);
|
$linesA = preg_split('/\r\n|\r|\n/', (string) $a['content']);
|
||||||
$linesB = preg_split('/\r\n|\r|\n/', (string) $b['content']);
|
$linesB = preg_split('/\r\n|\r|\n/', (string) $b['content']);
|
||||||
$d = self::lcsLineDiff($linesA, $linesB);
|
$setA = array_count_values(array_map('trim', $linesA));
|
||||||
$this->jsonSuccess([
|
$setB = array_count_values(array_map('trim', $linesB));
|
||||||
'from' => ['id' => $fromId, 'version' => $a['version']],
|
|
||||||
'to' => ['id' => $toId, 'version' => $b['version']],
|
|
||||||
'added' => $d['added'],
|
|
||||||
'removed' => $d['removed'],
|
|
||||||
'summary' => ['added' => count($d['added']), 'removed' => count($d['removed'])],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Diff line-by-line basato su LCS (Longest Common Subsequence).
|
|
||||||
* Rileva correttamente righe duplicate, spostamenti e modifiche posizionali,
|
|
||||||
* a differenza di un confronto set-based. Restituisce le righe aggiunte (in B
|
|
||||||
* ma non allineate in A) e rimosse (in A ma non allineate in B), con posizione.
|
|
||||||
*
|
|
||||||
* @return array{added: list<array{line:int,text:string}>, removed: list<array{line:int,text:string}>}
|
|
||||||
*/
|
|
||||||
private static function lcsLineDiff(array $a, array $b): array
|
|
||||||
{
|
|
||||||
$n = count($a);
|
|
||||||
$m = count($b);
|
|
||||||
// Tabella LCS (lunghezze). Cap di sicurezza per evitare blow-up su documenti enormi.
|
|
||||||
if ($n > 4000 || $m > 4000) {
|
|
||||||
// fallback prudente: confronto posizionale semplice riga per riga
|
|
||||||
$added = []; $removed = [];
|
|
||||||
$max = max($n, $m);
|
|
||||||
for ($i = 0; $i < $max; $i++) {
|
|
||||||
$la = $a[$i] ?? null; $lb = $b[$i] ?? null;
|
|
||||||
if ($la !== $lb) {
|
|
||||||
if ($lb !== null && trim($lb) !== '') $added[] = ['line' => $i + 1, 'text' => $lb];
|
|
||||||
if ($la !== null && trim($la) !== '') $removed[] = ['line' => $i + 1, 'text' => $la];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ['added' => $added, 'removed' => $removed];
|
|
||||||
}
|
|
||||||
|
|
||||||
$dp = array_fill(0, $n + 1, array_fill(0, $m + 1, 0));
|
|
||||||
for ($i = $n - 1; $i >= 0; $i--) {
|
|
||||||
for ($j = $m - 1; $j >= 0; $j--) {
|
|
||||||
$dp[$i][$j] = ($a[$i] === $b[$j])
|
|
||||||
? $dp[$i + 1][$j + 1] + 1
|
|
||||||
: max($dp[$i + 1][$j], $dp[$i][$j + 1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$added = []; $removed = [];
|
$added = []; $removed = [];
|
||||||
$i = 0; $j = 0;
|
foreach ($linesB as $l) { $t = trim($l); if ($t !== '' && empty($setA[$t])) $added[] = $l; }
|
||||||
while ($i < $n && $j < $m) {
|
foreach ($linesA as $l) { $t = trim($l); if ($t !== '' && empty($setB[$t])) $removed[] = $l; }
|
||||||
if ($a[$i] === $b[$j]) {
|
$this->jsonSuccess([
|
||||||
$i++; $j++;
|
'from' => ['id' => $fromId, 'version' => $a['version']],
|
||||||
} elseif ($dp[$i + 1][$j] >= $dp[$i][$j + 1]) {
|
'to' => ['id' => $toId, 'version' => $b['version']],
|
||||||
if (trim($a[$i]) !== '') $removed[] = ['line' => $i + 1, 'text' => $a[$i]];
|
'added' => $added,
|
||||||
$i++;
|
'removed' => $removed,
|
||||||
} else {
|
'summary' => ['added' => count($added), 'removed' => count($removed)],
|
||||||
if (trim($b[$j]) !== '') $added[] = ['line' => $j + 1, 'text' => $b[$j]];
|
]);
|
||||||
$j++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (; $i < $n; $i++) { if (trim($a[$i]) !== '') $removed[] = ['line' => $i + 1, 'text' => $a[$i]]; }
|
|
||||||
for (; $j < $m; $j++) { if (trim($b[$j]) !== '') $added[] = ['line' => $j + 1, 'text' => $b[$j]]; }
|
|
||||||
return ['added' => $added, 'removed' => $removed];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** GET /api/policies/pending-attestations — policy approvate non ancora attestate dall'utente. */
|
/** GET /api/policies/pending-attestations — policy approvate non ancora attestate dall'utente. */
|
||||||
|
|||||||
@ -2369,40 +2369,7 @@ class ServicesController extends BaseController
|
|||||||
$data['final_report_due'] = date('Y-m-d H:i:s', $t + 30 * 86400);
|
$data['final_report_due'] = date('Y-m-d H:i:s', $t + 30 * 86400);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert resiliente: retry su collisione incident_code (random 6 cifre),
|
$incidentId = Database::insert('incidents', $data);
|
||||||
// e gestione graceful della race su external_ref (dedup concorrente → 200).
|
|
||||||
$incidentId = null;
|
|
||||||
for ($attempt = 0; $attempt < 5; $attempt++) {
|
|
||||||
try {
|
|
||||||
$incidentId = Database::insert('incidents', $data);
|
|
||||||
break;
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
$msg = $e->getMessage();
|
|
||||||
if (stripos($msg, 'Duplicate') === false && stripos($msg, '1062') === false) {
|
|
||||||
throw $e; // errore non-duplicate: propaga
|
|
||||||
}
|
|
||||||
// Race su external_ref: un altro processo ha gia ingerito lo stesso alert
|
|
||||||
if ($externalRef !== null && stripos($msg, 'uq_incident_external_ref') !== false) {
|
|
||||||
$dup = Database::fetchOne(
|
|
||||||
'SELECT id, incident_code FROM incidents WHERE organization_id = ? AND external_ref = ?',
|
|
||||||
[$orgId, $externalRef]
|
|
||||||
);
|
|
||||||
if ($dup) {
|
|
||||||
$this->jsonSuccess([
|
|
||||||
'id' => (int) $dup['id'],
|
|
||||||
'incident_code' => $dup['incident_code'],
|
|
||||||
'deduplicated' => true,
|
|
||||||
], 'Alert già ingerito (dedup concorrente)', 200);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Altrimenti: collisione su incident_code → rigenera e riprova
|
|
||||||
$data['incident_code'] = $this->generateCode('INC');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($incidentId === null) {
|
|
||||||
$this->jsonError('Impossibile registrare l\'incidente (codice duplicato)', 500, 'INSERT_FAILED');
|
|
||||||
}
|
|
||||||
|
|
||||||
Database::insert('incident_timeline', [
|
Database::insert('incident_timeline', [
|
||||||
'incident_id' => $incidentId,
|
'incident_id' => $incidentId,
|
||||||
@ -2659,28 +2626,23 @@ class ServicesController extends BaseController
|
|||||||
$this->requireApiKey('read:compliance');
|
$this->requireApiKey('read:compliance');
|
||||||
$orgId = $this->currentOrgId;
|
$orgId = $this->currentOrgId;
|
||||||
|
|
||||||
// Auto-stale: controlli la cui ultima evidenza è scaduta vanno marcati stale.
|
// Auto-stale: controlli la cui ultima evidenza è scaduta vanno marcati stale
|
||||||
// Degrada con grazia se la tabella evidenze non esiste (coerente con AuditController).
|
Database::query(
|
||||||
try {
|
"UPDATE compliance_controls cc
|
||||||
Database::query(
|
LEFT JOIN (
|
||||||
"UPDATE compliance_controls cc
|
SELECT t.control_code, t.valid_until, t.status
|
||||||
LEFT JOIN (
|
FROM control_evidence_auto t
|
||||||
SELECT t.control_code, t.valid_until, t.status
|
JOIN (SELECT control_code, MAX(collected_at) mx FROM control_evidence_auto
|
||||||
FROM control_evidence_auto t
|
WHERE organization_id = ? GROUP BY control_code) m
|
||||||
JOIN (SELECT control_code, MAX(collected_at) mx FROM control_evidence_auto
|
ON m.control_code = t.control_code AND m.mx = t.collected_at
|
||||||
WHERE organization_id = ? GROUP BY control_code) m
|
WHERE t.organization_id = ?
|
||||||
ON m.control_code = t.control_code AND m.mx = t.collected_at
|
) last ON last.control_code = cc.control_code
|
||||||
WHERE t.organization_id = ?
|
SET cc.monitoring_status = 'stale'
|
||||||
) last ON last.control_code = cc.control_code
|
WHERE cc.organization_id = ?
|
||||||
SET cc.monitoring_status = 'stale'
|
AND cc.monitoring_status NOT IN ('not_monitored')
|
||||||
WHERE cc.organization_id = ?
|
AND last.valid_until IS NOT NULL AND last.valid_until < NOW()",
|
||||||
AND cc.monitoring_status NOT IN ('not_monitored')
|
[$orgId, $orgId, $orgId]
|
||||||
AND last.valid_until IS NOT NULL AND last.valid_until < NOW()",
|
);
|
||||||
[$orgId, $orgId, $orgId]
|
|
||||||
);
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
error_log('[CCM-services] ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
$rows = Database::fetchAll(
|
$rows = Database::fetchAll(
|
||||||
'SELECT control_code, title, status, monitoring_status, last_checked_at, freshness_days, implementation_percentage
|
'SELECT control_code, title, status, monitoring_status, last_checked_at, freshness_days, implementation_percentage
|
||||||
|
|||||||
@ -27,16 +27,6 @@ Utente vuole: "le credenziali si configurano nella card per ogni azienda cliente
|
|||||||
|
|
||||||
### Stato git fine sessione: origin/main = `6c8f8e2`, ahead 0. Tutto su Gitea.
|
### Stato git fine sessione: origin/main = `6c8f8e2`, ahead 0. Tutto su Gitea.
|
||||||
|
|
||||||
### REVIEW MULTI-AGENTE (5 agenti) + FIX — fine sessione 2026-05-30
|
|
||||||
Lanciati 5 agenti read-only di review (P1, P2, P3, connettori, frontend). Esito: nessuna vuln critica nelle feature, ma trovati problemi reali. **Tutti corretti, testati E2E, pushati.** Stato finale: **origin/main = `dac41f8`, ahead 0**.
|
|
||||||
- 🔴 `8e3a2c1` **risks.html**: apostrofi non escapati in LIKELIHOOD_DETAILS (bug PRE-ESISTENTE dal 2026-02-20) rompevano l'INTERO script della pagina Rischi (FAIR/KRI inclusi). Fix + verificato LIVE.
|
|
||||||
- 🔴 `9f4d2a1` **Connettori (SEC)**: (1) `connectorOrgGuard` usava `users.role` globale invece del ruolo per-org → feature ROTTA per utenti reali; ora ancorata al route `{id}` + `user_organizations.role`. (2) secret-strip denylist→**allowlist** ricorsiva. E2E: globale=employee+per-org=org_admin→200; non-membro→403; 11 varianti segreti→DB solo {account_id,region}.
|
|
||||||
- 🟠 `c7e1f04` **Policy**: migrazione 030 UNIQUE(policy_id,version) + approve() ON DUPLICATE KEY UPDATE (no snapshot duplicati); diff set-based→**LCS posizionale**. E2E ok.
|
|
||||||
- 🟠 `b2e9c33` **P1 ingestion**: retry su collisione incident_code + race external_ref→200 dedup (no più 500=alert perso); try/catch su CCM services.
|
|
||||||
- 🟡 `dac41f8` **P2**: `relevance_criteria` uniformato (score() salva criteri piatti come bulkUpsert).
|
|
||||||
- ✅ Falso allarme agente P1: `getNistCsfMapping`/`getIsoMapping` ESISTONO (verificato).
|
|
||||||
- Migrazioni totali sessione: 023-030. ⚠️ Le 027/028/029 NON erano state pushate come gusci? No: tutte applicate in prod + codice completo dopo i fix.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## SESSIONE 2026-05-30 mattina (~07:50→09:15 CEST) — Chiusura gap competitivi Evix (backend)
|
## SESSIONE 2026-05-30 mattina (~07:50→09:15 CEST) — Chiusura gap competitivi Evix (backend)
|
||||||
|
|||||||
@ -1,36 +0,0 @@
|
|||||||
-- ============================================================================
|
|
||||||
-- Migration 030 - UNIQUE(policy_id, version) su policy_versions (P3 hardening)
|
|
||||||
-- ----------------------------------------------------------------------------
|
|
||||||
-- Evita snapshot duplicati della stessa versione di policy (riapprovazioni
|
|
||||||
-- ripetute con la stessa version creavano righe duplicate, confondendo il diff).
|
|
||||||
-- Idempotente: aggiunge l'indice solo se assente; de-duplica prima se necessario.
|
|
||||||
--
|
|
||||||
-- mysql -h localhost nis2_agile_db -e "source docs/sql/030_policy_versions_unique.sql"
|
|
||||||
-- ============================================================================
|
|
||||||
|
|
||||||
-- 1) Rimuovi eventuali duplicati pre-esistenti (mantieni il piu recente per id)
|
|
||||||
DELETE pv1 FROM policy_versions pv1
|
|
||||||
JOIN policy_versions pv2
|
|
||||||
ON pv1.policy_id = pv2.policy_id
|
|
||||||
AND pv1.version = pv2.version
|
|
||||||
AND pv1.id < pv2.id;
|
|
||||||
|
|
||||||
-- 2) Aggiungi UNIQUE solo se non esiste gia
|
|
||||||
DELIMITER //
|
|
||||||
DROP PROCEDURE IF EXISTS _mig030 //
|
|
||||||
CREATE PROCEDURE _mig030()
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (
|
|
||||||
SELECT 1 FROM information_schema.STATISTICS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'policy_versions'
|
|
||||||
AND INDEX_NAME = 'uq_policy_version'
|
|
||||||
) THEN
|
|
||||||
ALTER TABLE policy_versions ADD UNIQUE KEY uq_policy_version (policy_id, version);
|
|
||||||
END IF;
|
|
||||||
END //
|
|
||||||
DELIMITER ;
|
|
||||||
CALL _mig030();
|
|
||||||
DROP PROCEDURE IF EXISTS _mig030;
|
|
||||||
|
|
||||||
-- ROLLBACK:
|
|
||||||
-- ALTER TABLE policy_versions DROP INDEX uq_policy_version;
|
|
||||||
Loading…
Reference in New Issue
Block a user