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 제외.
107 lines
6.3 KiB
PHP
107 lines
6.3 KiB
PHP
<?php
|
|
// =============================================================================
|
|
// ops_checks.php - shared operational/security self-check helpers
|
|
// =============================================================================
|
|
|
|
require_once __DIR__ . '/config.php';
|
|
require_once __DIR__ . '/admin_security.php';
|
|
|
|
function ops_table_exists(PDO $db, string $table): bool {
|
|
$stmt = $db->prepare("
|
|
SELECT COUNT(*)
|
|
FROM information_schema.TABLES
|
|
WHERE TABLE_SCHEMA = DATABASE()
|
|
AND TABLE_NAME = :table
|
|
");
|
|
$stmt->execute([':table' => $table]);
|
|
return (int)$stmt->fetchColumn() > 0;
|
|
}
|
|
|
|
function ops_column_exists(PDO $db, string $table, string $column): bool {
|
|
$stmt = $db->prepare("
|
|
SELECT COUNT(*)
|
|
FROM information_schema.COLUMNS
|
|
WHERE TABLE_SCHEMA = DATABASE()
|
|
AND TABLE_NAME = :table
|
|
AND COLUMN_NAME = :column
|
|
");
|
|
$stmt->execute([':table' => $table, ':column' => $column]);
|
|
return (int)$stmt->fetchColumn() > 0;
|
|
}
|
|
|
|
function ops_check(string $category, string $label, bool $ok, string $detail, string $fix = '', string $severity = 'warn', string $evidence = ''): array {
|
|
return [
|
|
'category' => $category,
|
|
'label' => $label,
|
|
'ok' => $ok,
|
|
'detail' => $detail,
|
|
'fix' => $fix,
|
|
'severity' => $severity,
|
|
'evidence' => $evidence,
|
|
];
|
|
}
|
|
|
|
function collect_ops_checks(): array {
|
|
$checks = [];
|
|
|
|
try {
|
|
$db = get_db();
|
|
$checks[] = ops_check('서버', 'DB 연결', true, DB_NAME . ' 연결 성공', '', 'critical', 'setup_wizard.php 또는 security_evidence.php 화면');
|
|
|
|
$required_tables = ['sensor_log', 'sensor_status', 'sensor_metric', 'sms_log'];
|
|
foreach ($required_tables as $table) {
|
|
$exists = ops_table_exists($db, $table);
|
|
$checks[] = ops_check(
|
|
'DB',
|
|
"테이블 {$table}",
|
|
$exists,
|
|
$exists ? '확인됨' : '누락됨',
|
|
'sql/schema_sht30.sql 실행',
|
|
'critical',
|
|
'DB 테이블 목록 또는 설치 점검 화면'
|
|
);
|
|
}
|
|
|
|
// 최근 온습도 측정 수신 여부(센서 동작/수집 확인)
|
|
if (ops_table_exists($db, 'sensor_metric')) {
|
|
$recent_metric_stmt = $db->query("
|
|
SELECT COUNT(*)
|
|
FROM sensor_metric
|
|
WHERE measured_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
|
|
");
|
|
$recent_metric = (int)$recent_metric_stmt->fetchColumn();
|
|
$checks[] = ops_check('센서', '최근 1시간 온습도 수신', $recent_metric > 0, "{$recent_metric}건", 'SHT30 기기 전원/네트워크/배선 및 sensor_data.php 수신 확인', 'warn', 'sensor_metric 최근 수신');
|
|
}
|
|
|
|
if (ops_table_exists($db, 'sms_log')) {
|
|
$recent_sms_fail_stmt = $db->query("
|
|
SELECT COUNT(*)
|
|
FROM sms_log
|
|
WHERE status = 'fail'
|
|
AND sent_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
|
|
");
|
|
$recent_sms_fail = (int)$recent_sms_fail_stmt->fetchColumn();
|
|
$checks[] = ops_check('SMS', '최근 30일 SMS 실패', $recent_sms_fail === 0, "{$recent_sms_fail}건", 'Cafe24 SMS 계정, 잔액, 발신번호, 수신자 확인', 'warn', 'sms_log 최근 실패 건수');
|
|
}
|
|
} catch (Throwable $e) {
|
|
$checks[] = ops_check('서버', 'DB 연결', false, '연결 실패: ' . $e->getMessage(), 'config.local.php DB 설정 확인', 'critical', 'DB 연결 오류 화면');
|
|
}
|
|
|
|
$default_recipients = count(SMS_RECIPIENTS) === 1 && preg_replace('/\D+/', '', SMS_RECIPIENTS[0]) === '01000000000';
|
|
$checks[] = ops_check('설정', 'API 키', API_KEY !== 'change-this-api-key', 'API_KEY 운영값 설정 여부', 'config.local.php와 /etc/sht30-sensor.env(또는 펌웨어 secrets.h)의 API 키를 동일하게 설정', 'critical', 'config.local.php 원문 미공개 설정 확인');
|
|
$checks[] = ops_check('설정', '관리자 비밀번호', ADMIN_PASSWORD_HASH !== '', 'ADMIN_PASSWORD_HASH 설정 여부', 'php setup_hash.php "new-password" 실행 후 config.local.php에 반영', 'critical', '관리자 해시 설정 확인');
|
|
$checks[] = ops_check('관리자 인증', '관리자 MFA', admin_mfa_configured(), 'ADMIN_TOTP_SECRET 설정 여부', '인증 앱용 Base32 비밀키를 config.local.php에 설정', 'critical', 'MFA 등록 확인표');
|
|
$checks[] = ops_check('관리자 인증', '감사로그', is_file(admin_audit_log_path()) || is_writable(__DIR__ . '/var'), admin_audit_log_path(), '로그인 성공/실패, 로그아웃, 관리자 활동 기록 확인', 'warn', 'php/var/admin_audit.log');
|
|
$checks[] = ops_check('개인정보', 'SMS 수신자', !empty(SMS_RECIPIENTS) && !$default_recipients, count(SMS_RECIPIENTS) . '명 설정', 'config.local.php의 SMS_RECIPIENTS 현행화', 'critical', '수신자 목록 현행화 확인표');
|
|
$thr_ok = (METRIC_TEMP_LOW_C < METRIC_TEMP_HIGH_C) && (METRIC_RH_LOW < METRIC_RH_HIGH);
|
|
$checks[] = ops_check('설정', '온습도 임계치', $thr_ok, sprintf('온도 %s~%s℃ / 습도 %s~%s%%', METRIC_TEMP_LOW_C, METRIC_TEMP_HIGH_C, METRIC_RH_LOW, METRIC_RH_HIGH), 'config.local.php의 METRIC_* 임계치를 운영값으로 설정(저온<고온, 저습<고습)', 'warn', '임계치 설정 확인');
|
|
$checks[] = ops_check('파일', '상태 파일 폴더', is_dir(__DIR__ . '/var') && is_writable(__DIR__ . '/var'), __DIR__ . '/var', 'php/var 생성 및 쓰기 권한 확인', 'warn', '폴더 권한 화면');
|
|
$checks[] = ops_check('보관기간', '보관기간 정리', is_file(__DIR__ . '/retention_cleanup.php'), 'SMS ' . SMS_LOG_RETENTION_DAYS . '일 / 센서로그 ' . SENSOR_LOG_RETENTION_DAYS . '일 / 측정 ' . SENSOR_METRIC_RETENTION_DAYS . '일', 'php retention_cleanup.php --dry-run 결과 확인 후 --execute 수행', 'warn', 'retention_cleanup.php 실행 결과');
|
|
$checks[] = ops_check('백업', '백업 증적', is_file(dirname(__DIR__) . '/scripts/backup_evidence.php'), 'scripts/backup_evidence.php', '백업 파일 목록과 복구 테스트 결과를 Markdown으로 보관', 'warn', 'backup-evidence 보고서');
|
|
|
|
return $checks;
|
|
}
|
|
|
|
function ops_failed_count(array $checks): int {
|
|
return count(array_filter($checks, fn($check) => !$check['ok']));
|
|
}
|