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 제외.
832 lines
28 KiB
Markdown
832 lines
28 KiB
Markdown
# Operational Security Completion Implementation Plan
|
|
|
|
> 참고: 이 문서는 누수감지 시절의 기록(레거시)이며, 현재 시스템은 SHT30 온습도 전용으로 전환되었습니다.
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Turn the v2605 leak monitoring project into a submission-ready and field-operable system with concrete security evidence, operational diagnostics, and review-friendly reports.
|
|
|
|
**Architecture:** Keep the current procedural PHP/Cafe24 structure. Extract reusable operational checks into a small PHP helper, add a login-protected evidence report page, add a Pi-side sanitized evidence collector, and update documentation so security controls map to actual code and captured evidence.
|
|
|
|
**Tech Stack:** PHP 7+/PDO/MySQL on Cafe24, Apache `.htaccess`, Raspberry Pi Python 3, systemd, Markdown documentation.
|
|
|
|
---
|
|
|
|
## Origin
|
|
|
|
Source requirements: `docs/superpowers/specs/2026-05-20-operational-security-completion-design.md`
|
|
|
|
This plan intentionally avoids implementing fire detection, power anomaly detection, multi-tenant productization, or HWP binary editing. Those remain outside the current scope.
|
|
|
|
## File Structure
|
|
|
|
Create:
|
|
|
|
- `php/ops_checks.php`
|
|
Shared server-side operational/security checks used by setup and evidence pages.
|
|
|
|
- `php/security_evidence.php`
|
|
Login-protected evidence report page for security review attachments. Supports browser view and Markdown download.
|
|
|
|
- `raspberry_pi/collect_evidence.py`
|
|
Sanitized Pi-side evidence collector that prints service, network, camera, and API test evidence without exposing secrets.
|
|
|
|
- `docs/OPERATIONS_SECURITY_CHECKLIST.md`
|
|
Operator checklist for monthly security/operation review.
|
|
|
|
- `docs/SECURITY_EVIDENCE_PACKAGE.md`
|
|
Submission package index: what to attach, where it comes from, and what it proves.
|
|
|
|
Modify:
|
|
|
|
- `php/setup_wizard.php`
|
|
Use shared checks and link to `security_evidence.php`.
|
|
|
|
- `php/dashboard.php`
|
|
Add clearer links/status hints for evidence, SMS failures, and offline state without changing its core flow.
|
|
|
|
- `php/monthly_report.php`
|
|
Add print-friendly metadata and evidence-oriented summary rows.
|
|
|
|
- `docs/README.md`
|
|
Link the new evidence and operations checklist docs.
|
|
|
|
- `docs/SECURITY_PLAN_ATTACHMENT_GUIDE.md`
|
|
Narrow the user-provided items now that code-based evidence is generated locally.
|
|
|
|
- `docs/SOURCE_SECURITY_EVIDENCE.md`
|
|
Point to the new evidence page and Pi collector.
|
|
|
|
- `CHANGELOG.md`
|
|
Record the operational/security completion improvements.
|
|
|
|
Verification:
|
|
|
|
- PHP lint for all changed PHP files.
|
|
- Python compile for Raspberry Pi scripts.
|
|
- Static secret/stale-value search excluding `_backup`.
|
|
- Manual browser verification on deployed Cafe24 remains an operator step, documented but not locally provable.
|
|
|
|
---
|
|
|
|
## Task 1: Extract Shared Operational Checks
|
|
|
|
**Files:**
|
|
- Create: `php/ops_checks.php`
|
|
- Modify: `php/setup_wizard.php`
|
|
- Test: PHP lint plus manual inclusion through `setup_wizard.php`
|
|
|
|
- [ ] **Step 1: Create `php/ops_checks.php` with reusable checks**
|
|
|
|
Create a small helper that returns normalized check rows. Use this structure:
|
|
|
|
```php
|
|
<?php
|
|
// =============================================================================
|
|
// ops_checks.php - 운영/보안 점검 공통 헬퍼 (v2605)
|
|
// =============================================================================
|
|
|
|
require_once __DIR__ . '/config.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', 'sms_log', 'leak_photo', 'leak_incident'];
|
|
foreach ($required_tables as $table) {
|
|
$exists = ops_table_exists($db, $table);
|
|
$checks[] = ops_check(
|
|
'DB',
|
|
"테이블 {$table}",
|
|
$exists,
|
|
$exists ? '확인됨' : '누락됨',
|
|
$table === 'leak_incident' ? 'sql/migration_incident_response.sql 또는 sql/schema_v2605.sql 실행' : 'sql/schema_v2605.sql 실행',
|
|
$table === 'leak_incident' ? 'critical' : 'warn',
|
|
'DB 테이블 목록 또는 설치 점검 화면'
|
|
);
|
|
}
|
|
|
|
if (ops_table_exists($db, 'leak_photo')) {
|
|
foreach (['sequence_no', 'photo_stage'] as $column) {
|
|
$exists = ops_column_exists($db, 'leak_photo', $column);
|
|
$checks[] = ops_check('DB', "사진 타임라인 컬럼 {$column}", $exists, $exists ? '확인됨' : '누락됨', 'sql/migration_photo_timeline.sql 실행', 'warn', 'leak_photo 컬럼 목록');
|
|
}
|
|
}
|
|
|
|
$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/leak-sensor.env의 API 키를 동일하게 설정', 'critical', 'config.local.php 원문 미공개 설정 확인');
|
|
$checks[] = ops_check('설정', '관리자 비밀번호', ADMIN_PASSWORD_HASH !== '', 'ADMIN_PASSWORD_HASH 설정 여부', 'php setup_hash.php "새비밀번호" 실행 후 config.local.php에 반영', 'critical', '관리자 해시 설정 확인');
|
|
$checks[] = ops_check('개인정보', 'SMS 수신자', !empty(SMS_RECIPIENTS) && !$default_recipients, count(SMS_RECIPIENTS) . '명 설정', 'config.local.php의 SMS_RECIPIENTS 현행화', 'critical', '수신자 목록 현행화 확인표');
|
|
$checks[] = ops_check('파일', '사진 저장 폴더', is_dir(PHOTO_UPLOAD_DIR) && is_writable(PHOTO_UPLOAD_DIR), PHOTO_UPLOAD_DIR, 'uploads/photos 생성 및 쓰기 권한 확인', 'warn', '폴더 권한 화면');
|
|
$checks[] = ops_check('파일', '상태 파일 폴더', is_dir(__DIR__ . '/var') && is_writable(__DIR__ . '/var'), __DIR__ . '/var', 'php/var 생성 및 쓰기 권한 확인', 'warn', '폴더 권한 화면');
|
|
|
|
return $checks;
|
|
}
|
|
|
|
function ops_failed_count(array $checks): int {
|
|
return count(array_filter($checks, fn($check) => !$check['ok']));
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Replace duplicate setup check functions in `php/setup_wizard.php`**
|
|
|
|
Remove `setup_table_exists()`, `setup_column_exists()`, and `setup_check()` from `setup_wizard.php`. Add:
|
|
|
|
```php
|
|
require_once __DIR__ . '/ops_checks.php';
|
|
```
|
|
|
|
Replace the current manual `$checks` construction with:
|
|
|
|
```php
|
|
$checks = collect_ops_checks();
|
|
```
|
|
|
|
Keep the existing SMS test POST handling unchanged.
|
|
|
|
- [ ] **Step 3: Update setup wizard rendering for new check fields**
|
|
|
|
Where checks render, preserve the existing UI but read the new fields:
|
|
|
|
```php
|
|
<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; ?>
|
|
```
|
|
|
|
- [ ] **Step 4: Add evidence page link to setup wizard**
|
|
|
|
In the header link area, add a link:
|
|
|
|
```php
|
|
<a href="security_evidence.php">보안 증적</a>
|
|
```
|
|
|
|
If the header currently has a single link, convert it to a small flex group matching `dashboard.php`.
|
|
|
|
- [ ] **Step 5: Verify PHP syntax**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
php -l php\ops_checks.php
|
|
php -l php\setup_wizard.php
|
|
```
|
|
|
|
Expected: both commands print `No syntax errors detected`.
|
|
|
|
---
|
|
|
|
## Task 2: Add Login-Protected Security Evidence Report
|
|
|
|
**Files:**
|
|
- Create: `php/security_evidence.php`
|
|
- Modify: `php/.htaccess`
|
|
- Test: PHP lint and login-protected manual browser check
|
|
|
|
- [ ] **Step 1: Create `php/security_evidence.php`**
|
|
|
|
Create a login-required report page that uses `collect_ops_checks()` and supports Markdown download using `?format=md`.
|
|
|
|
Core structure:
|
|
|
|
```php
|
|
<?php
|
|
// =============================================================================
|
|
// security_evidence.php - 보안대책서 첨부 증적 보고서 (v2605)
|
|
// =============================================================================
|
|
|
|
require_once __DIR__ . '/config.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;
|
|
}
|
|
|
|
$checks = collect_ops_checks();
|
|
$failed_count = ops_failed_count($checks);
|
|
$generated_at = date('Y-m-d H:i:s');
|
|
|
|
$control_rows = [
|
|
['사업 범위', '1차 구축 범위를 누수·오프라인·사진·SMS·대시보드로 한정', '보안대책서 사업개요', 'SECURITY_PLAN_PASS_READINESS.md'],
|
|
['전송구간', 'HTTPS/TLS 사용, API 서명 검증', 'sensor_data.php, photo_upload.php, leak_sensor.py', 'SOURCE_SECURITY_EVIDENCE.md'],
|
|
['API 인증', '공유 API 키와 요청 서명 검증', 'config.php, leak_sensor.py', '403 테스트 결과'],
|
|
['비밀값', 'config.local.php와 /etc/leak-sensor.env로 분리', 'config.php, config.py', '설정 파일 권한 화면'],
|
|
['관리자 인증', '비밀번호 해시, CSRF, 세션 보호, 실패 제한', 'login.php', '로그인 화면'],
|
|
['사진정보', 'JPEG 검증, 로그 ID 검증, 업로드 실행 차단', 'photo_upload.php, uploads/photos/.htaccess', '사진 업로드 결과'],
|
|
['로그관리', '센서/SMS/사진/사고 로그 저장', 'MySQL', '월간 보고서'],
|
|
['장애대응', '미확인 사고 재알림, 장비 오프라인 감지', 'incidents.php, cron_heartbeat.php', 'SMS 로그, 사고 이력'],
|
|
];
|
|
|
|
if (($_GET['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 '| ' . $check['category'] . ' | ' . $check['label'] . ' | ' . ($check['ok'] ? '정상' : '확인 필요') . ' | ' . str_replace('|', '/', $check['detail']) . ' | ' . str_replace('|', '/', $check['fix']) . ' | ' . str_replace('|', '/', $check['evidence']) . " |\n";
|
|
}
|
|
echo "\n## 보안통제 매트릭스\n\n";
|
|
echo "| 영역 | 보안대책 | 구현 위치 | 확인 증적 |\n";
|
|
echo "|---|---|---|---|\n";
|
|
foreach ($control_rows as $row) {
|
|
echo '| ' . implode(' | ', $row) . " |\n";
|
|
}
|
|
exit;
|
|
}
|
|
?>
|
|
```
|
|
|
|
Render the HTML body with:
|
|
|
|
- title: `보안 증적 보고서`
|
|
- summary card: generated time, `APP_VERSION`, failed count
|
|
- operational checks table
|
|
- security control matrix table
|
|
- download link: `security_evidence.php?format=md`
|
|
- print button: `window.print()`
|
|
|
|
- [ ] **Step 2: Ensure helper files remain directly blocked**
|
|
|
|
Modify `php/.htaccess` `FilesMatch` to include `ops_checks`:
|
|
|
|
```apache
|
|
<FilesMatch "^(config|config\.local|config\.local\.example|sms_send|incidents|ops_checks|setup_hash|test_mobile)\.php$">
|
|
```
|
|
|
|
Do not block `security_evidence.php`; it is a login-protected page.
|
|
|
|
- [ ] **Step 3: Verify PHP syntax**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
php -l php\security_evidence.php
|
|
```
|
|
|
|
Expected:
|
|
|
|
- `security_evidence.php` passes PHP lint.
|
|
- `.htaccess` is not PHP; do not lint it. Instead visually confirm `ops_checks` is in the `FilesMatch` rule.
|
|
|
|
- [ ] **Step 4: Manual browser test after deployment**
|
|
|
|
Expected behavior:
|
|
|
|
- Unauthenticated `security_evidence.php` redirects to `login.php`.
|
|
- Authenticated access displays checks and control matrix.
|
|
- `?format=md` downloads Markdown without secrets.
|
|
|
|
---
|
|
|
|
## Task 3: Add Raspberry Pi Evidence Collector
|
|
|
|
**Files:**
|
|
- Create: `raspberry_pi/collect_evidence.py`
|
|
- Modify: `docs/INSTALL_PI_SERVER.md`
|
|
- Test: Python compile and dry-run on development machine
|
|
|
|
- [ ] **Step 1: Create `raspberry_pi/collect_evidence.py`**
|
|
|
|
The script must not print API keys, passwords, or SMS secrets. It should report whether required env vars exist, not their values.
|
|
|
|
Implementation outline:
|
|
|
|
```python
|
|
#!/usr/bin/env python3
|
|
"""Sanitized Raspberry Pi evidence collector for security review attachments."""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import time
|
|
from pathlib import Path
|
|
|
|
|
|
ENV_FILE = Path("/etc/leak-sensor.env")
|
|
|
|
|
|
def run_command(command, timeout=10):
|
|
try:
|
|
result = subprocess.run(command, capture_output=True, text=True, timeout=timeout)
|
|
return {
|
|
"command": " ".join(command),
|
|
"returncode": result.returncode,
|
|
"stdout": result.stdout.strip()[:4000],
|
|
"stderr": result.stderr.strip()[:2000],
|
|
}
|
|
except Exception as exc:
|
|
return {"command": " ".join(command), "returncode": -1, "stdout": "", "stderr": str(exc)}
|
|
|
|
|
|
def env_presence():
|
|
keys = [
|
|
"LEAK_API_URL",
|
|
"LEAK_PHOTO_UPLOAD_URL",
|
|
"LEAK_API_KEY",
|
|
"LEAK_DEVICE_ID",
|
|
"LEAK_DEVICE_LOCATION",
|
|
"LEAK_CAMERA_ENABLED",
|
|
"LEAK_PHOTO_TIMELINE_ENABLED",
|
|
"LEAK_PHOTO_TIMELINE_DELAYS",
|
|
]
|
|
return {key: bool(os.environ.get(key)) for key in keys}
|
|
|
|
|
|
def collect():
|
|
evidence = {
|
|
"generated_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
"hostname": run_command(["hostname"])["stdout"],
|
|
"env_file_exists": ENV_FILE.exists(),
|
|
"env_file_permission": run_command(["ls", "-l", str(ENV_FILE)]) if ENV_FILE.exists() else None,
|
|
"env_presence": env_presence(),
|
|
"service_status": run_command(["systemctl", "status", "leak-sensor", "--no-pager"], timeout=15),
|
|
"service_enabled": run_command(["systemctl", "is-enabled", "leak-sensor"], timeout=10),
|
|
"listening_ports": run_command(["ss", "-lntup"], timeout=10),
|
|
"camera_tools": {
|
|
"rpicam-still": shutil.which("rpicam-still") is not None,
|
|
"libcamera-still": shutil.which("libcamera-still") is not None,
|
|
},
|
|
"recent_logs": run_command(["journalctl", "-u", "leak-sensor", "-n", "80", "--no-pager"], timeout=15),
|
|
}
|
|
return evidence
|
|
|
|
|
|
def print_markdown(evidence):
|
|
print("# Raspberry Pi 보안/운영 증적")
|
|
print()
|
|
print(f"- 생성시각: {evidence['generated_at']}")
|
|
print(f"- 호스트명: {evidence['hostname']}")
|
|
print(f"- 환경파일 존재: {'예' if evidence['env_file_exists'] else '아니오'}")
|
|
if evidence["env_file_permission"]:
|
|
print(f"- 환경파일 권한: `{evidence['env_file_permission']['stdout']}`")
|
|
print()
|
|
print("## 환경변수 설정 여부")
|
|
print()
|
|
print("| 항목 | 설정 여부 |")
|
|
print("|---|---|")
|
|
for key, present in evidence["env_presence"].items():
|
|
print(f"| {key} | {'설정됨' if present else '미설정'} |")
|
|
print()
|
|
print("## 서비스 상태")
|
|
print()
|
|
print("```text")
|
|
print(evidence["service_status"]["stdout"] or evidence["service_status"]["stderr"])
|
|
print("```")
|
|
print()
|
|
print("## 수신 포트")
|
|
print()
|
|
print("```text")
|
|
print(evidence["listening_ports"]["stdout"] or evidence["listening_ports"]["stderr"])
|
|
print("```")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--format", choices=["json", "md"], default="md")
|
|
args = parser.parse_args()
|
|
evidence = collect()
|
|
if args.format == "json":
|
|
print(json.dumps(evidence, ensure_ascii=False, indent=2))
|
|
else:
|
|
print_markdown(evidence)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
```
|
|
|
|
- [ ] **Step 2: Document Pi evidence command in `docs/INSTALL_PI_SERVER.md`**
|
|
|
|
Add under the Pi final verification section:
|
|
|
|
```bash
|
|
cd /home/pi/leak_sensor
|
|
source venv/bin/activate
|
|
python3 collect_evidence.py --format md > pi-security-evidence.md
|
|
```
|
|
|
|
Note that the output intentionally does not include raw API keys.
|
|
|
|
- [ ] **Step 3: Verify Python syntax**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
python -m py_compile raspberry_pi\collect_evidence.py
|
|
```
|
|
|
|
Expected: exit code 0 and no output.
|
|
|
|
- [ ] **Step 4: Dry-run on non-Pi development machine**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
python raspberry_pi\collect_evidence.py --format json
|
|
```
|
|
|
|
Expected:
|
|
|
|
- JSON output is produced.
|
|
- systemd/camera commands may show errors on Windows; this is acceptable if the script does not crash.
|
|
- No API key value is printed.
|
|
|
|
---
|
|
|
|
## Task 4: Improve Report and Dashboard Evidence Quality
|
|
|
|
**Files:**
|
|
- Modify: `php/monthly_report.php`
|
|
- Modify: `php/dashboard.php`
|
|
- Test: PHP lint and visual/manual verification after deployment
|
|
|
|
- [ ] **Step 1: Add report metadata to `php/monthly_report.php`**
|
|
|
|
Add near the existing `$summary` block:
|
|
|
|
```php
|
|
$report_generated_at = date('Y-m-d H:i:s');
|
|
$report_scope = '누수 감지, 장비 오프라인, SMS, 사진, 사고 대응';
|
|
```
|
|
|
|
Render above the summary cards:
|
|
|
|
```php
|
|
<section class="card" style="margin-bottom:18px">
|
|
<span>보고서 생성</span>
|
|
<strong style="font-size:1rem"><?= htmlspecialchars($report_generated_at) ?></strong>
|
|
<p style="color:#64748b;font-size:.84rem;margin-top:8px">범위: <?= htmlspecialchars($report_scope) ?></p>
|
|
</section>
|
|
```
|
|
|
|
This makes printed/PDF reports usable as evidence.
|
|
|
|
- [ ] **Step 2: Extend monthly CSV with evidence metadata**
|
|
|
|
Before existing CSV rows, add:
|
|
|
|
```php
|
|
fputcsv($out, ['generated_at', $report_generated_at]);
|
|
fputcsv($out, ['scope', $report_scope]);
|
|
```
|
|
|
|
Expected CSV begins with month, generated time, and scope.
|
|
|
|
- [ ] **Step 3: Add dashboard link to evidence page**
|
|
|
|
In `php/dashboard.php` header links, add:
|
|
|
|
```php
|
|
<a href="security_evidence.php" style="color:#fff;font-size:.85rem;text-decoration:none;background:rgba(255,255,255,.15);padding:6px 14px;border-radius:6px;">보안 증적</a>
|
|
```
|
|
|
|
Keep existing `setup_wizard.php`, `monthly_report.php`, and logout links.
|
|
|
|
- [ ] **Step 4: Add a short evidence hint near dashboard diagnostics**
|
|
|
|
In the operational diagnostics section, add one concise sentence:
|
|
|
|
```php
|
|
<p style="color:#64748b;font-size:.84rem;margin-top:8px">보안대책서 첨부용 점검표는 상단의 보안 증적 화면에서 내려받을 수 있습니다.</p>
|
|
```
|
|
|
|
- [ ] **Step 5: Verify PHP syntax**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
php -l php\monthly_report.php
|
|
php -l php\dashboard.php
|
|
```
|
|
|
|
Expected: both commands print `No syntax errors detected`.
|
|
|
|
---
|
|
|
|
## Task 5: Finalize Documentation Package
|
|
|
|
**Files:**
|
|
- Create: `docs/OPERATIONS_SECURITY_CHECKLIST.md`
|
|
- Create: `docs/SECURITY_EVIDENCE_PACKAGE.md`
|
|
- Modify: `docs/README.md`
|
|
- Modify: `docs/SECURITY_PLAN_ATTACHMENT_GUIDE.md`
|
|
- Modify: `docs/SOURCE_SECURITY_EVIDENCE.md`
|
|
- Modify: `CHANGELOG.md`
|
|
- Test: Markdown link/path sanity checks
|
|
|
|
- [ ] **Step 1: Create `docs/OPERATIONS_SECURITY_CHECKLIST.md`**
|
|
|
|
Include this structure:
|
|
|
|
```markdown
|
|
# 운영·보안 점검 체크리스트
|
|
|
|
## 월간 점검
|
|
|
|
| 점검 항목 | 기준 | 증적 |
|
|
|---|---|---|
|
|
| 대시보드 로그인 | 관리자만 접근 가능 | 로그인 화면, 접근통제 확인 |
|
|
| 설치 점검 | 확인 필요 항목 없음 또는 조치 계획 존재 | setup_wizard.php 캡처 |
|
|
| 보안 증적 | 점검 결과와 보안통제 매트릭스 확인 | security_evidence.php 캡처 또는 MD 다운로드 |
|
|
| Pi 서비스 | leak-sensor active 상태 | collect_evidence.py 결과 |
|
|
| SMS 발송 | 최근 실패 건수 확인 | monthly_report.php 또는 sms_log |
|
|
| 사진 저장 | 사진 타임라인 확인 | dashboard.php 캡처 |
|
|
| 백업 | DB/사진/설정 백업 확인 | 백업 파일 목록 |
|
|
| 보관기간 | 사진/로그 기간 초과분 정리 | 삭제/정리 기록 |
|
|
|
|
## 사고 발생 시
|
|
|
|
1. 대시보드에서 사고 상태와 사진 타임라인을 확인한다.
|
|
2. SMS 수신 여부와 `sms_log` 실패 여부를 확인한다.
|
|
3. 현장 조치 후 사고 상태를 `조치 완료` 또는 `오탐`으로 기록한다.
|
|
4. 월간 보고서에 사고 처리 내역이 반영됐는지 확인한다.
|
|
```
|
|
|
|
- [ ] **Step 2: Create `docs/SECURITY_EVIDENCE_PACKAGE.md`**
|
|
|
|
Include:
|
|
|
|
```markdown
|
|
# 보안대책서 첨부자료 패키지
|
|
|
|
## 필수 첨부
|
|
|
|
| 첨부자료 | 생성 위치 | 증명하는 것 |
|
|
|---|---|---|
|
|
| 보안 증적 보고서 | `php/security_evidence.php?format=md` | 보안통제와 운영 점검 연결 |
|
|
| 설치 점검 화면 | `php/setup_wizard.php` | DB/권한/SMS/설정 상태 |
|
|
| 대시보드 화면 | `php/dashboard.php` | 현재 위험, 사고, 사진, 장비 상태 |
|
|
| 월간 보고서 | `php/monthly_report.php` | SMS/사진/사고 처리 현황 |
|
|
| Pi 증적 보고서 | `raspberry_pi/collect_evidence.py --format md` | Pi 서비스, 포트, 환경파일 상태 |
|
|
| HTTPS 접속 화면 | 운영 도메인 브라우저 | 전송구간 보호 적용 |
|
|
|
|
## 제출 전 마스킹
|
|
|
|
- API 키 원문 제거
|
|
- DB 비밀번호 제거
|
|
- SMS secure key 제거
|
|
- 관리자 비밀번호 원문 제거
|
|
- 담당자 휴대전화번호는 필요 시 뒷자리 마스킹
|
|
```
|
|
|
|
- [ ] **Step 3: Update `docs/README.md`**
|
|
|
|
Add the new documents under folder structure:
|
|
|
|
```text
|
|
OPERATIONS_SECURITY_CHECKLIST.md # 월간 운영·보안 점검표
|
|
SECURITY_EVIDENCE_PACKAGE.md # 보안대책서 첨부자료 패키지
|
|
```
|
|
|
|
Add an operations section that points to:
|
|
|
|
- `setup_wizard.php`
|
|
- `security_evidence.php`
|
|
- `monthly_report.php`
|
|
- `raspberry_pi/collect_evidence.py`
|
|
|
|
- [ ] **Step 4: Update security guide docs**
|
|
|
|
In `docs/SECURITY_PLAN_ATTACHMENT_GUIDE.md`, add that API/source evidence is generated by:
|
|
|
|
```text
|
|
php/security_evidence.php
|
|
raspberry_pi/collect_evidence.py
|
|
docs/SOURCE_SECURITY_EVIDENCE.md
|
|
```
|
|
|
|
In `docs/SOURCE_SECURITY_EVIDENCE.md`, add:
|
|
|
|
```text
|
|
운영 서버 반영 후 `security_evidence.php?format=md`를 내려받으면 코드 기반 보안통제와 운영 점검 결과를 하나의 증적 파일로 보관할 수 있다.
|
|
```
|
|
|
|
- [ ] **Step 5: Update `CHANGELOG.md`**
|
|
|
|
Add under v2605:
|
|
|
|
```markdown
|
|
- 보안대책서 첨부용 증적 보고서 계획과 운영·보안 체크리스트를 추가했습니다.
|
|
- Pi 증적 수집 스크립트 계획을 추가해 API 키 원문 없이 서비스/포트/환경파일 상태를 확인할 수 있게 했습니다.
|
|
```
|
|
|
|
- [ ] **Step 6: Verify Markdown paths**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
Get-ChildItem docs -Recurse -File | Where-Object { $_.Extension -eq '.md' } | Select-String -Pattern 'security_evidence.php|collect_evidence.py|OPERATIONS_SECURITY_CHECKLIST|SECURITY_EVIDENCE_PACKAGE'
|
|
```
|
|
|
|
Expected: new docs and links appear in README/security docs.
|
|
|
|
---
|
|
|
|
## Task 6: Full Verification Pass
|
|
|
|
**Files:**
|
|
- All changed PHP, Python, Markdown files
|
|
|
|
- [ ] **Step 1: Run PHP lint**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
$files = @(
|
|
'php\config.php',
|
|
'php\ops_checks.php',
|
|
'php\setup_wizard.php',
|
|
'php\security_evidence.php',
|
|
'php\dashboard.php',
|
|
'php\monthly_report.php',
|
|
'php\api\sensor_data.php',
|
|
'php\api\photo_upload.php',
|
|
'php\login.php',
|
|
'php\sms_send.php',
|
|
'php\incidents.php',
|
|
'php\cron_heartbeat.php'
|
|
)
|
|
foreach ($file in $files) { php -l $file }
|
|
```
|
|
|
|
Expected: every file reports `No syntax errors detected`.
|
|
|
|
- [ ] **Step 2: Run Python compile checks**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
python -m py_compile raspberry_pi\config.py raspberry_pi\leak_sensor.py raspberry_pi\test_connection.py raspberry_pi\collect_evidence.py scripts\generate_security_plan_images.py
|
|
```
|
|
|
|
Expected: exit code 0 and no output.
|
|
|
|
- [ ] **Step 3: Search for stale secrets and legacy values**
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
Get-ChildItem -Recurse -File |
|
|
Where-Object { $_.FullName -notlike '*\_backup\*' } |
|
|
Select-String -Pattern '<legacy-patterns-redacted>' -CaseSensitive:$false
|
|
```
|
|
|
|
Expected: no matches outside intentional documentation examples.
|
|
|
|
- [ ] **Step 4: Confirm no Python cache remains**
|
|
|
|
If `py_compile` creates cache files, remove them after verifying the resolved path is inside the workspace:
|
|
|
|
```powershell
|
|
$target = Resolve-Path -LiteralPath 'raspberry_pi\__pycache__' -ErrorAction SilentlyContinue
|
|
if ($target) {
|
|
$workspace = (Resolve-Path -LiteralPath '.').Path
|
|
if ($target.Path.StartsWith($workspace)) {
|
|
Remove-Item -LiteralPath $target.Path -Recurse -Force
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 5: Manual deployment verification checklist**
|
|
|
|
After uploading to Cafe24 and Pi:
|
|
|
|
- Visit `login.php`; verify login is required.
|
|
- Visit `setup_wizard.php`; capture the result.
|
|
- Visit `security_evidence.php`; capture the result.
|
|
- Download `security_evidence.php?format=md`; confirm no secrets are present.
|
|
- Visit `dashboard.php`; confirm evidence link and diagnostics.
|
|
- Visit `monthly_report.php`; print/PDF preview includes generated time and scope.
|
|
- Run `python3 collect_evidence.py --format md` on Pi; confirm no API key is printed.
|
|
- Run `python3 test_connection.py` on Pi; confirm unsigned request is rejected and signed request succeeds.
|
|
|
|
Expected: these items produce the minimum security evidence package.
|
|
|
|
---
|
|
|
|
## Task 7: Completion Notes
|
|
|
|
**Files:**
|
|
- Modify: `docs/SECURITY_PLAN_PASS_READINESS.md`
|
|
- Modify: `docs/SECURITY_PLAN_HWP_REVIEW.md`
|
|
|
|
- [ ] **Step 1: Point final security docs to the new evidence outputs**
|
|
|
|
In `docs/SECURITY_PLAN_PASS_READINESS.md`, add references to:
|
|
|
|
- `php/security_evidence.php`
|
|
- `php/security_evidence.php?format=md`
|
|
- `raspberry_pi/collect_evidence.py --format md`
|
|
- `docs/SECURITY_EVIDENCE_PACKAGE.md`
|
|
|
|
- [ ] **Step 2: Clarify remaining user-provided evidence**
|
|
|
|
Keep the remaining user-provided list short:
|
|
|
|
```text
|
|
1. 실제 서버 도메인과 HTTPS 화면
|
|
2. Pi 네트워크 분리 또는 방화벽 정책 확인
|
|
3. 설치 위치 사진과 자산 정보
|
|
4. 백업 주기와 담당자
|
|
5. SMS 수신자 현행화 확인
|
|
```
|
|
|
|
- [ ] **Step 3: Verify final docs contain no over-claim**
|
|
|
|
Search:
|
|
|
|
```powershell
|
|
Select-String -Path docs\SECURITY_PLAN_*.md,docs\SOURCE_SECURITY_EVIDENCE.md -Pattern '국정원.*통과|완벽|보장|반드시 통과'
|
|
```
|
|
|
|
Expected: no text claims guaranteed approval.
|
|
|
|
---
|
|
|
|
## Coverage Map
|
|
|
|
| Requirement | Implemented by |
|
|
|---|---|
|
|
| 심사 대응 패키지 | Tasks 1, 2, 5, 7 |
|
|
| 코드 기반 보안근거 | Tasks 2, 5, 7 |
|
|
| 운영 실패 가시화 | Tasks 1, 3, 4 |
|
|
| 대시보드/보고서 증적 품질 | Task 4 |
|
|
| 자동 증적 생성 | Tasks 2, 3 |
|
|
| 확장 범위 분리 | Task 7 plus existing security docs |
|
|
| 비밀값 미노출 | Tasks 2, 3, 6 |
|
|
|
|
## Execution Notes
|
|
|
|
- This workspace currently has no `.git` directory, so commit steps are skipped in this workspace.
|
|
- Do not put real API keys, DB passwords, SMS secure keys, or administrator passwords into Markdown.
|
|
- If a deployed Cafe24 behavior cannot be verified locally, document it as an operator evidence requirement instead of claiming it as verified.
|