query( 'SELECT id, actor, action, target, before_json, after_json, ts, prev_hash, entry_hash FROM move_audit_log ORDER BY id ASC' )->fetchAll(PDO::FETCH_ASSOC); $total = count($rows); $unchained = 0; // 마이그레이션 이전 행 (해시 없음) $verified = 0; $breaks = []; // ['id' => .., 'why' => ..] $expectedPrev = null; $chainStarted = false; foreach ($rows as $r) { $id = (int)$r['id']; if ($r['entry_hash'] === null || $r['entry_hash'] === '') { if ($chainStarted) { // 체인이 시작된 뒤에 해시 없는 행이 나타났다 = 사슬을 우회해 삽입된 행. $breaks[] = ['id' => $id, 'why' => '해시 없는 행이 체인 구간에 삽입됨']; } else { $unchained++; } continue; } if (!$chainStarted) { // 체인의 첫 행. prev_hash 는 NULL(제네시스)이거나 직전 구간이 없어 NULL 이다. $chainStarted = true; } elseif (($r['prev_hash'] ?? null) !== $expectedPrev) { $breaks[] = [ 'id' => $id, 'why' => 'prev_hash 불일치 — 직전 행이 수정되었거나 삭제됨 (기대 ' . substr((string)$expectedPrev, 0, 12) . '…, 실제 ' . substr((string)($r['prev_hash'] ?? 'NULL'), 0, 12) . '…)', ]; } $recomputed = PdoAuditRepository::hash( (string)$r['actor'], (string)$r['action'], (string)$r['target'], (string)$r['before_json'], (string)$r['after_json'], (int)$r['ts'], $r['prev_hash'] !== null && $r['prev_hash'] !== '' ? (string)$r['prev_hash'] : null ); if (!hash_equals((string)$r['entry_hash'], $recomputed)) { $breaks[] = ['id' => $id, 'why' => '행 내용이 자신의 해시와 불일치 — 사후 수정됨']; } else { $verified++; } $expectedPrev = (string)$r['entry_hash']; } $ok = $breaks === []; $report = [ 'ok' => $ok, 'total_rows' => $total, 'verified' => $verified, 'pre_chain_rows' => $unchained, 'breaks' => $breaks, ]; if ($asJson) { echo json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), "\n"; exit($ok ? 0 : 1); } echo "감사로그 사슬 검증\n"; echo " 전체 행 : {$total}\n"; echo " 검증 완료 : {$verified}\n"; echo " 검증 불가 구간: {$unchained}" . ($unchained > 0 ? " (해시체인 도입 이전에 기록된 행)" : "") . "\n"; if ($ok) { echo "\n무결합니다 — 사슬이 끊긴 곳이 없습니다.\n"; exit(0); } echo "\n손상 " . count($breaks) . "건:\n"; foreach ($breaks as $b) { echo " - id={$b['id']}: {$b['why']}\n"; } echo "\n이 지점 전후의 기록은 신뢰할 수 없습니다. DB 백업과 대조하십시오.\n"; exit(1);