SHT30 온습도 모니터링 시스템 전체 소스(서버 PHP, STM32 펌웨어, SQL, 테스트). 전체 코드리뷰에서 도출된 보안 하드닝 10건 반영: - 요청 서명 HMAC-SHA256 전환(펌웨어 sig.c/서버 config.php/호스트 패리티 동시) - 재전송 방어 + 기본 API_KEY fail-closed + 디바이스 문자열 정제(api/sensor_data.php) - 오프라인 SMS 중복 발송 경합 제거(cron_heartbeat.php, 원자적 선점) - CSV 수식 주입 방지(monthly_report.php), 감사로그 회전 락(retention_cleanup.php) - 브루트포스 카운터 원자화(login.php), 예시 TOTP 비밀키 무효화, 마이그레이션 멱등화 _backup/(하드코딩 실 비밀값 포함)·config.local.php·런타임 상태는 .gitignore 제외.
122 lines
4.9 KiB
PHP
122 lines
4.9 KiB
PHP
<?php
|
|
// =============================================================================
|
|
// retention_cleanup.php - 로그/측정 보관기간 정리 (SMS 로그, 센서 로그, 측정 이력,
|
|
// 관리자 감사로그)
|
|
//
|
|
// Usage:
|
|
// php retention_cleanup.php --dry-run
|
|
// php retention_cleanup.php --execute
|
|
// =============================================================================
|
|
|
|
if (php_sapi_name() !== 'cli') {
|
|
http_response_code(403);
|
|
header('Content-Type: text/plain; charset=utf-8');
|
|
echo "Forbidden\n";
|
|
exit;
|
|
}
|
|
|
|
require_once __DIR__ . '/config.php';
|
|
require_once __DIR__ . '/admin_security.php';
|
|
|
|
$execute = in_array('--execute', $argv, true);
|
|
$dryRun = !$execute || in_array('--dry-run', $argv, true);
|
|
|
|
function retention_cutoff(int $days): string {
|
|
return date('Y-m-d H:i:s', time() - ($days * 86400));
|
|
}
|
|
|
|
function retention_count(PDO $db, string $sql, array $params): int {
|
|
$stmt = $db->prepare($sql);
|
|
$stmt->execute($params);
|
|
return (int)$stmt->fetchColumn();
|
|
}
|
|
|
|
function retention_delete(PDO $db, string $sql, array $params, bool $dryRun): int {
|
|
if ($dryRun) {
|
|
return 0;
|
|
}
|
|
$stmt = $db->prepare($sql);
|
|
$stmt->execute($params);
|
|
return $stmt->rowCount();
|
|
}
|
|
|
|
$summary = [
|
|
'mode' => $dryRun ? 'dry-run' : 'execute',
|
|
'sms_log_retention_days' => SMS_LOG_RETENTION_DAYS,
|
|
'sensor_log_retention_days' => SENSOR_LOG_RETENTION_DAYS,
|
|
'sensor_metric_retention_days' => SENSOR_METRIC_RETENTION_DAYS,
|
|
'admin_audit_retention_days' => ADMIN_AUDIT_RETENTION_DAYS,
|
|
'sms_log' => ['matched' => 0, 'rows_deleted' => 0],
|
|
'sensor_log' => ['matched' => 0, 'rows_deleted' => 0],
|
|
'sensor_metric' => ['matched' => 0, 'rows_deleted' => 0],
|
|
'admin_audit' => ['kept_lines' => null, 'removed_lines' => null],
|
|
];
|
|
|
|
try {
|
|
$db = get_db();
|
|
} catch (Throwable $e) {
|
|
$summary['error'] = 'DB connection failed: ' . $e->getMessage();
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
echo json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . PHP_EOL;
|
|
exit(2);
|
|
}
|
|
|
|
$smsCutoff = retention_cutoff(SMS_LOG_RETENTION_DAYS);
|
|
$summary['sms_log']['matched'] = retention_count($db, "SELECT COUNT(*) FROM sms_log WHERE sent_at < :cutoff", [':cutoff' => $smsCutoff]);
|
|
$summary['sms_log']['rows_deleted'] = retention_delete($db, "DELETE FROM sms_log WHERE sent_at < :cutoff", [':cutoff' => $smsCutoff], $dryRun);
|
|
|
|
$sensorCutoff = retention_cutoff(SENSOR_LOG_RETENTION_DAYS);
|
|
$summary['sensor_log']['matched'] = retention_count($db, "SELECT COUNT(*) FROM sensor_log WHERE created_at < :cutoff", [':cutoff' => $sensorCutoff]);
|
|
$summary['sensor_log']['rows_deleted'] = retention_delete($db, "DELETE FROM sensor_log WHERE created_at < :cutoff", [':cutoff' => $sensorCutoff], $dryRun);
|
|
|
|
$metricCutoff = retention_cutoff(SENSOR_METRIC_RETENTION_DAYS);
|
|
$summary['sensor_metric']['matched'] = retention_count($db, "SELECT COUNT(*) FROM sensor_metric WHERE created_at < :cutoff", [':cutoff' => $metricCutoff]);
|
|
$summary['sensor_metric']['rows_deleted'] = retention_delete($db, "DELETE FROM sensor_metric WHERE created_at < :cutoff", [':cutoff' => $metricCutoff], $dryRun);
|
|
|
|
$auditPath = admin_audit_log_path();
|
|
if (is_file($auditPath)) {
|
|
$cutoffTs = time() - (ADMIN_AUDIT_RETENTION_DAYS * 86400);
|
|
$kept = [];
|
|
$removed = 0;
|
|
// 감사로그 회전은 동시 append(admin_audit_log: file_put_contents FILE_APPEND|LOCK_EX)와
|
|
// 경합한다. 읽기+필터+재기록 전체를 배타 락(flock LOCK_EX)으로 묶어, 회전 도중
|
|
// 들어온 기록이 유실되지 않게 한다(append 는 락 해제까지 대기 후 새 파일에 추가).
|
|
$fh = fopen($auditPath, 'c+');
|
|
if ($fh !== false && flock($fh, LOCK_EX)) {
|
|
while (($line = fgets($fh)) !== false) {
|
|
$line = rtrim($line, "\r\n");
|
|
if ($line === '') {
|
|
continue;
|
|
}
|
|
$record = json_decode($line, true);
|
|
$recordTs = isset($record['time']) ? strtotime((string)$record['time']) : false;
|
|
if ($recordTs !== false && $recordTs < $cutoffTs) {
|
|
$removed++;
|
|
continue;
|
|
}
|
|
$kept[] = $line;
|
|
}
|
|
if (!$dryRun && $removed > 0) {
|
|
ftruncate($fh, 0);
|
|
rewind($fh);
|
|
fwrite($fh, $kept ? implode(PHP_EOL, $kept) . PHP_EOL : '');
|
|
fflush($fh);
|
|
}
|
|
flock($fh, LOCK_UN);
|
|
}
|
|
if ($fh !== false) {
|
|
fclose($fh);
|
|
}
|
|
$summary['admin_audit']['kept_lines'] = count($kept);
|
|
$summary['admin_audit']['removed_lines'] = $removed;
|
|
}
|
|
|
|
admin_audit_log('retention_cleanup', 'system', true, [
|
|
'mode' => $summary['mode'],
|
|
'sms_matches' => $summary['sms_log']['matched'],
|
|
'sensor_matches' => $summary['sensor_log']['matched'],
|
|
'metric_matches' => $summary['sensor_metric']['matched'],
|
|
]);
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
echo json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . PHP_EOL;
|