POSA_LEAKSMS/php/security_evidence.php
cw2007 73e49fd528 feat: RPi4 누수+온습도 2-모듈 단일 서버 통합 + 보안 하드닝
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>
2026-06-23 16:21:11 +09:00

156 lines
8.2 KiB
PHP

<?php
// =============================================================================
// security_evidence.php - security-control evidence report
// =============================================================================
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/admin_security.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(); // 유휴 + 절대 수명 타임아웃(대시보드 외 페이지에서도 강제)
$checks = collect_ops_checks();
$failed_count = ops_failed_count($checks);
$generated_at = date('Y-m-d H:i:s');
$control_rows = [
['사업 범위', '서버실 온습도(SHT30) 모니터링 및 임계치 초과 SMS 경보, 장비 오프라인 알림', '보안대책서 사업개요', 'SECURITY_PLAN_PASS_READINESS.md'],
['전송구간', 'HTTPS/TLS 사용, API 서명 검증', 'api/sensor_data.php, config.php', 'SOURCE_SECURITY_EVIDENCE.md'],
['API 인증', '공유 API 키 기반 요청 서명 검증', 'config.php', '403 테스트 결과'],
['비밀값', 'config.local.php와 /etc/sht30-sensor.env로 분리', 'config.php', '설정 파일 권한 화면'],
['관리자 인증', '비밀번호 해시, CSRF, 세션 보호, 실패 제한, 관리자 MFA', 'login.php, admin_security.php', '로그인 화면, MFA 등록 확인표'],
['감사로그', '로그인 성공/실패, 로그아웃, 관리자 활동 접속기록 저장', 'admin_security.php, php/var/admin_audit.log', '감사로그 파일'],
['로그관리', '센서 측정 이력(sensor_metric)·SMS 발송 이력(sms_log)·관리자 감사로그 저장 및 보관기간 정리', 'MySQL, retention_cleanup.php', '보관기간 정리 dry-run 결과'],
['장애대응', '온습도 임계 경보 SMS + 장비 오프라인/복구 알림', 'cron_heartbeat.php', 'SMS 로그, sensor_metric 이력'],
['백업', 'DB/설정 백업 목록과 복구 테스트 증적 관리', 'scripts/backup_evidence.php', 'backup-evidence 보고서'],
];
function md_cell(string $value): string {
return str_replace(["\r", "\n", '|'], [' ', ' ', '/'], $value);
}
if (($_GET['format'] ?? '') === 'md') {
admin_audit_log('security_evidence_download', $_SESSION['username'] ?? ADMIN_USER, true, ['format' => 'md']);
header('Content-Type: text/markdown; charset=utf-8');
header('Content-Disposition: attachment; filename="security-evidence-' . date('Ymd-His') . '.md"');
echo "# 온습도 모니터링 시스템 보안 증적 보고서\n\n";
echo "- 생성시각: {$generated_at}\n";
echo "- 버전: " . APP_VERSION . "\n";
echo "- 평가 결과: " . ($failed_count === 0 ? "전체 통과" : "확인 필요 {$failed_count}건") . "\n\n";
echo "## 운영 점검 결과\n\n";
echo "| 분류 | 항목 | 결과 | 상세 | 조치 | 증적 |\n";
echo "|---|---|---|---|---|---|\n";
foreach ($checks as $check) {
echo '| ' . md_cell($check['category']) . ' | ' . md_cell($check['label']) . ' | ' . ($check['ok'] ? '정상' : '확인 필요') . ' | ' . md_cell($check['detail']) . ' | ' . md_cell($check['fix']) . ' | ' . md_cell($check['evidence']) . " |\n";
}
echo "\n## 보안통제 매트릭스\n\n";
echo "| 영역 | 보안대책 | 구현 위치 | 확인 증적 |\n";
echo "|---|---|---|---|\n";
foreach ($control_rows as $row) {
echo '| ' . implode(' | ', array_map('md_cell', $row)) . " |\n";
}
exit;
}
?>
<!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; gap: 12px; }
header h1 { font-size: 1.15rem; margin: 0; }
header a, .btn { color: #fff; text-decoration: none; background: rgba(255,255,255,.15); padding: 7px 12px; border-radius: 6px; font-size: .85rem; border: 0; cursor: pointer; }
.container { max-width: 1120px; 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' ?>; }
.actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
.actions a, .actions button { background: #1a56db; color: #fff; border: 0; border-radius: 6px; padding: 8px 12px; text-decoration: none; font-weight: 700; cursor: pointer; }
table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 4px rgba(0,0,0,.08); margin-bottom: 24px; }
th, td { border-bottom: 1px solid #e2e8f0; padding: 9px 10px; text-align: left; font-size: .84rem; vertical-align: top; }
th { background: #f8fafc; color: #475569; }
h2 { font-size: 1rem; color: #334155; margin: 24px 0 10px; }
.ok { color: #166534; font-weight: 700; }
.fail { color: #b91c1c; font-weight: 700; }
.muted { color: #64748b; font-size: .84rem; }
@media print {
body { background: #fff; }
header, .actions { display: none; }
.container { margin: 0; max-width: none; }
table, .summary { box-shadow: none; }
}
@media (max-width: 760px) {
header { align-items: flex-start; flex-direction: column; }
table { display: block; overflow-x: auto; }
}
</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="setup_wizard.php">설치 점검</a>
</div>
</header>
<main class="container">
<section class="summary">
<strong><?= $failed_count === 0 ? '전체 점검 통과' : "확인 필요 {$failed_count}건" ?></strong>
<p class="muted">생성시각: <?= htmlspecialchars($generated_at) ?> · 버전: <?= htmlspecialchars(APP_VERSION) ?></p>
<p class="muted">이 보고서는 보안대책서 첨부용 운영 점검 결과와 보안통제 매트릭스를 제공합니다. 비밀번호, API 키, SMS 인증키 원문은 표시하지 않습니다.</p>
<div class="actions">
<a href="?format=md">Markdown 다운로드</a>
<button type="button" onclick="window.print()">인쇄/PDF</button>
</div>
</section>
<h2>운영 점검 결과</h2>
<table>
<thead>
<tr><th>분류</th><th>항목</th><th>결과</th><th>상세</th><th>조치</th><th>증적</th></tr>
</thead>
<tbody>
<?php foreach ($checks as $check): ?>
<tr>
<td><?= htmlspecialchars($check['category']) ?></td>
<td><?= htmlspecialchars($check['label']) ?></td>
<td class="<?= $check['ok'] ? 'ok' : 'fail' ?>"><?= $check['ok'] ? '정상' : '확인 필요' ?></td>
<td><?= htmlspecialchars($check['detail']) ?></td>
<td><?= htmlspecialchars($check['fix'] ?: '-') ?></td>
<td><?= htmlspecialchars($check['evidence'] ?: '-') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<h2>보안통제 매트릭스</h2>
<table>
<thead>
<tr><th>영역</th><th>보안대책</th><th>구현 위치</th><th>확인 증적</th></tr>
</thead>
<tbody>
<?php foreach ($control_rows as $row): ?>
<tr>
<?php foreach ($row as $cell): ?>
<td><?= htmlspecialchars($cell) ?></td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</main>
</body>
</html>