2번 모듈 신규(raspberry_pi4_sht30/): SHT30 온습도 → cafe24, raw-body X-Signature HMAC, 폐쇄망 오프라인 설치(vendor/install), systemd, CRC/포맷 자가점검. firmware sht30_fw wire 포맷과 byte-parity 검증. 1번 모듈 통합(raspberry_pi4/): 누수+카메라를 동일 서버로. 레거시 sha256(key+json) body-sig → X-Signature HMAC 전환, 사진 canonical=device_id|sensor_id|log_id|timestamp. 서버 통합(php/): api/sensor_data.php 누수 이벤트 수신, api/photo_upload.php 신규(HMAC·finfo·서버생성 파일명·nonce 멱등·sensor 결속), photo.php 인증 사진서빙, dashboard.php 누수 패널+사진, sms_send.php 누수 SMS, config.php 태그/요약. DB: sql/migration_add_leak.sql(가산·멱등), schema_sht30.sql 통합 스키마. 보안 하드닝(외부 펜테스트 대비, 확정 CRITICAL/HIGH 0): cron_heartbeat api_key fail-closed, setup_mfa 부트스트랩 토큰 post-enrollment 잠금, var/ PHP 실행차단, HSTS. 문서/테스트: SYSTEM_OVERVIEW.md, security_hardening_test.php 갱신. 참고: 사용자 요청으로 세션 외 기존 미커밋 변경도 함께 포함. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
120 lines
5.6 KiB
PHP
120 lines
5.6 KiB
PHP
<?php
|
|
// =============================================================================
|
|
// setup_wizard.php - 설치/운영 점검 화면 (v2606, SHT30 온습도)
|
|
// =============================================================================
|
|
|
|
require_once __DIR__ . '/config.php';
|
|
require_once __DIR__ . '/sms_send.php';
|
|
require_once __DIR__ . '/ops_checks.php';
|
|
|
|
ini_set('session.cookie_httponly', 1);
|
|
ini_set('session.cookie_secure', 1);
|
|
ini_set('session.cookie_samesite', 'Strict');
|
|
ini_set('session.use_strict_mode', 1);
|
|
session_start();
|
|
|
|
if (empty($_SESSION['logged_in']) || $_SESSION['logged_in'] !== true) {
|
|
header('Location: login.php');
|
|
exit;
|
|
}
|
|
admin_enforce_session_timeout(); // 유휴 + 절대 수명 타임아웃(ops_checks → admin_security 경유 로드)
|
|
|
|
if (empty($_SESSION['csrf_token'])) {
|
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
|
}
|
|
|
|
$checks = collect_ops_checks();
|
|
$sms_test_result = null;
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'send_test_sms') {
|
|
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? '')) {
|
|
http_response_code(400);
|
|
echo 'Invalid request';
|
|
exit;
|
|
}
|
|
|
|
$message = "[온습도 모니터링 테스트]\n"
|
|
. "설치/운영 점검 문자입니다.\n"
|
|
. "시각: " . date('Y-m-d H:i:s');
|
|
$results = [];
|
|
foreach (SMS_RECIPIENTS as $receiver) {
|
|
$digits = preg_replace('/\D+/', '', $receiver);
|
|
$results[] = ['receiver' => $digits, 'result' => send_sms_cafe24($digits, $message)];
|
|
}
|
|
$sms_test_result = $results;
|
|
}
|
|
|
|
$failed_count = ops_failed_count($checks);
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>설치 점검 - 온습도 모니터링 <?= htmlspecialchars(APP_VERSION) ?></title>
|
|
<style>
|
|
* { box-sizing: border-box; }
|
|
body { margin: 0; font-family: 'Noto Sans KR', sans-serif; background: #f0f4f8; color: #1f2937; }
|
|
header { background: #1a56db; color: #fff; padding: 16px 24px; display: flex; justify-content: space-between; align-items: center; }
|
|
header h1 { font-size: 1.15rem; margin: 0; }
|
|
header a { color: #fff; text-decoration: none; background: rgba(255,255,255,.15); padding: 7px 12px; border-radius: 6px; font-size: .85rem; }
|
|
.container { max-width: 980px; margin: 24px auto; padding: 0 16px; }
|
|
.summary { background: #fff; border-radius: 8px; padding: 18px; box-shadow: 0 1px 4px rgba(0,0,0,.1); margin-bottom: 18px; }
|
|
.summary strong { font-size: 1.2rem; color: <?= $failed_count ? '#b91c1c' : '#166534' ?>; }
|
|
.check-list { display: grid; gap: 10px; }
|
|
.check { background: #fff; border-radius: 8px; padding: 14px 16px; box-shadow: 0 1px 4px rgba(0,0,0,.08); border-left: 6px solid #22c55e; }
|
|
.check.fail { border-left-color: #ef4444; }
|
|
.check h2 { margin: 0 0 6px; font-size: .98rem; }
|
|
.check p { margin: 0; color: #64748b; font-size: .84rem; }
|
|
.fix { margin-top: 7px; color: #92400e; font-size: .8rem; }
|
|
.actions { margin: 18px 0; display: flex; gap: 10px; flex-wrap: wrap; }
|
|
button { border: 0; background: #1a56db; color: #fff; padding: 10px 14px; border-radius: 7px; font-weight: 700; cursor: pointer; }
|
|
.result { background: #fff; border-radius: 8px; padding: 14px 16px; box-shadow: 0 1px 4px rgba(0,0,0,.08); margin-top: 18px; }
|
|
@media (max-width: 520px) { header { align-items: flex-start; flex-direction: column; gap: 10px; } }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>설치/운영 점검 <?= htmlspecialchars(APP_VERSION) ?></h1>
|
|
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
<a href="dashboard.php">대시보드</a>
|
|
<a href="security_evidence.php">보안 증적</a>
|
|
</div>
|
|
</header>
|
|
<main class="container">
|
|
<section class="summary">
|
|
<strong><?= $failed_count === 0 ? '전체 점검 통과' : "확인 필요 {$failed_count}건" ?></strong>
|
|
<div style="color:#64748b;font-size:.86rem;margin-top:6px">운영 반영 전 이 화면에서 설정, DB, 권한, SMS를 확인합니다.</div>
|
|
</section>
|
|
|
|
<div class="check-list">
|
|
<?php foreach ($checks as $check): ?>
|
|
<section class="check <?= $check['ok'] ? '' : 'fail' ?>">
|
|
<h2><?= htmlspecialchars($check['category']) ?> · <?= htmlspecialchars($check['label']) ?> · <?= $check['ok'] ? '정상' : '확인 필요' ?></h2>
|
|
<p><?= htmlspecialchars($check['detail']) ?></p>
|
|
<?php if (!$check['ok'] && $check['fix'] !== ''): ?>
|
|
<div class="fix">조치: <?= htmlspecialchars($check['fix']) ?></div>
|
|
<?php endif; ?>
|
|
<?php if (($check['evidence'] ?? '') !== ''): ?>
|
|
<div class="fix">증적: <?= htmlspecialchars($check['evidence']) ?></div>
|
|
<?php endif; ?>
|
|
</section>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
|
|
<form method="POST" class="actions">
|
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
|
<button type="submit" name="action" value="send_test_sms">테스트 SMS 발송</button>
|
|
</form>
|
|
|
|
<?php if ($sms_test_result !== null): ?>
|
|
<section class="result">
|
|
<strong>테스트 SMS 결과</strong>
|
|
<?php foreach ($sms_test_result as $row): ?>
|
|
<p><?= htmlspecialchars(mask_phone($row['receiver'])) ?> · <?= $row['result']['success'] ? '성공' : '실패' ?> · <?= htmlspecialchars($row['result']['message']) ?></p>
|
|
<?php endforeach; ?>
|
|
</section>
|
|
<?php endif; ?>
|
|
</main>
|
|
</body>
|
|
</html>
|