diff --git a/.hermes/plans/2026-09-04_091241-upload-checklist-config.md b/.hermes/plans/2026-09-04_091241-upload-checklist-config.md new file mode 100644 index 0000000..75a46f2 --- /dev/null +++ b/.hermes/plans/2026-09-04_091241-upload-checklist-config.md @@ -0,0 +1,38 @@ +# Upload Checklist Configuration Implementation Plan + +> **For Hermes:** Implement task-by-task with TDD and verify the Docker deployment contract. + +**Goal:** Bổ sung tại trang quản lý công cụ chức năng admin upload/thay thế cặp file checklist `.xlsx` và rule config `.json` cho từng OS. + +**Architecture:** Dùng mapping `OS_CONFIG` hiện có làm allowlist tên đích; không cho client tự chọn đường dẫn/tên file. Backend validate role admin, extension, JSON, tạo `.bak`, rồi ghi file vào `config/`. Đổi bind mount `config` sang read-write để chức năng hoạt động trong Docker. + +**Tech Stack:** FastAPI, Jinja2, Docker Compose, Python stdlib JSON/file I/O. + +--- + +### Task 1: Add regression tests +- Create `tests/test_checklist_config_upload.py`. +- Assert route exists, admin authorization is required, safe OS mapping is used, JSON is validated, and Compose config mount is writable. +- Run test and confirm RED. + +### Task 2: Implement secure upload endpoint +- Modify `app/web_app_fastapi.py`. +- Add `POST /admin/checklists/upload`. +- Require admin session. +- Accept `os_type`, optional checklist `.xlsx`, optional config `.json`; require at least one. +- Resolve destination only from `OS_CONFIG`. +- Validate JSON before write; write temporary file then atomic replace; back up existing files. +- Redirect with PRG status message. + +### Task 3: Add admin UI +- Modify `app/templates/admin_tools.html`. +- Add admin-only card showing current file names/status and multipart upload form. +- Analyzer remains limited to tool upload and cannot change checklist/rules. + +### Task 4: Enable persistence in Docker +- Modify `docker-compose.yml` from `./config:/app/config:ro` to `./config:/app/config`. +- Keep `config/` as the only writable configuration directory required by this feature. + +### Task 5: Verify +- Run regression tests, Python compile/AST, Jinja parse, JSON checks, `docker compose config`, and `git diff --check`. +- Grok execution review; fix any blocking findings. diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md new file mode 100644 index 0000000..f8ce9fa --- /dev/null +++ b/PROJECT_STATUS.md @@ -0,0 +1,49 @@ +# PROJECT STATUS — Audit Hardening Tool + +> Cập nhật lần cuối: 2026-09-04 09:12 (bởi: Hermes) +> Evaluator hiện tại: grok-build (grok-4.6) + +## Phase hiện tại +- [x] Phase 1 — Plan (`.hermes/plans/2026-09-04_091241-upload-checklist-config.md`) +- [x] Phase 2 — Xác định hướng xây dựng +- [x] Phase 3 — Thu thập kiến thức +- [x] Phase 4 — Thực hiện +- [x] Phase 5 — Tracking + +## Tổng quan +Bổ sung upload/thay thế checklist XLSX và rule JSON trên trang quản lý công cụ. Chức năng giới hạn admin, sử dụng mapping OS cố định và backup file cũ. + +## Việc đã xong +- [2026-09-04] Khảo sát `OS_CONFIG`, trang `admin_tools`, quyền admin/analyzer và bind mount `config`. +- [2026-09-04] Chốt thiết kế upload an toàn, không nhận path/tên đích từ client. +- [2026-09-04] Hoàn thành endpoint admin, UI upload checklist/rule, backup atomic và Docker RW mount; 8 regression tests pass. + +## Việc đang làm / Bước tiếp theo +- Commit/push lên Gitea, deploy NAS và kiểm thử upload thực tế qua Web. + +## Quyết định đã chốt +- Chỉ admin được upload checklist/rule config; analyzer chỉ upload tool. +- Checklist chỉ nhận `.xlsx`, rule config chỉ nhận JSON hợp lệ. +- Tên file đích lấy từ `OS_CONFIG`; không cho path traversal. +- Backup file hiện tại trước khi thay thế. + +## Đánh giá các phase +| Phase | Ngày | Evaluator | Kết quả | Ghi chú | +|-------|------|-----------|---------|---------| +| Plan | 2026-09-04 | grok-build | BLOCKED | Grok CLI chưa đăng nhập | +| Direction | 2026-09-04 | grok-build | BLOCKED | Grok CLI chưa đăng nhập | +| Knowledge | 2026-09-04 | Hermes | PASS | Dựa trên code/mount thực tế | +| Execution | 2026-09-04 | Hermes | PASS | 8 tests, compile, Jinja, Compose, diff check đều pass; Grok review bị chặn auth | + +## Kiến thức thu thập +- `OS_CONFIG` định nghĩa đúng cặp checklist/config cho Windows, CentOS, RHEL, Ubuntu, Oracle Linux. +- `config/` hiện bind mount read-only nên phải đổi sang RW để upload persistent. +- `require_admin_session` là guard phù hợp; analyzer không nên sửa rules/checklist. + +## Rủi ro / Vướng mắc +- File checklist/rule sai nội dung có thể làm xử lý audit lỗi; giảm thiểu bằng validation JSON, extension, backup và atomic replace. + +## Lệnh thường dùng +- `python -m py_compile app/web_app_fastapi.py` +- `docker compose config` +- `git diff --check` diff --git a/app/config_upload.py b/app/config_upload.py new file mode 100644 index 0000000..a065358 --- /dev/null +++ b/app/config_upload.py @@ -0,0 +1,49 @@ +import json +import os +import shutil +import zipfile +from io import BytesIO +from pathlib import Path +from typing import Iterable + + +def validate_xlsx(content: bytes) -> bytes: + """Validate that content is an XLSX-compatible ZIP containing workbook metadata.""" + try: + with zipfile.ZipFile(BytesIO(content)) as archive: + names = set(archive.namelist()) + except (zipfile.BadZipFile, OSError) as exc: + raise ValueError("Checklist không phải file XLSX hợp lệ") from exc + if "[Content_Types].xml" not in names or "xl/workbook.xml" not in names: + raise ValueError("Checklist không phải file XLSX hợp lệ") + return content + + +def normalize_json(content: bytes) -> bytes: + """Parse and normalize an object/array JSON configuration.""" + try: + parsed = json.loads(content.decode("utf-8-sig")) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise ValueError(f"Rule config JSON không hợp lệ: {exc}") from exc + if not isinstance(parsed, (dict, list)): + raise ValueError("Rule config JSON phải là object hoặc array") + return json.dumps(parsed, ensure_ascii=False, indent=2).encode("utf-8") + b"\n" + + +def install_config_files(config_root: str | Path, files: Iterable[tuple[str, bytes]]) -> None: + """Atomically replace allowlisted config files and keep one .bak copy.""" + root = Path(config_root) + root.mkdir(parents=True, exist_ok=True) + for target_name, content in files: + if Path(target_name).name != target_name: + raise ValueError("Tên file đích không hợp lệ") + target = root / target_name + temporary = root / f"{target_name}.tmp" + backup = root / f"{target_name}.bak" + with temporary.open("wb") as output: + output.write(content) + output.flush() + os.fsync(output.fileno()) + if target.exists(): + shutil.copy2(target, backup) + os.replace(temporary, target) diff --git a/app/templates/admin_tools.html b/app/templates/admin_tools.html index 2a4c73a..b0739dc 100644 --- a/app/templates/admin_tools.html +++ b/app/templates/admin_tools.html @@ -428,6 +428,38 @@ {% if user_info and user_info.role == 'admin' %} + +
+ Chọn hệ điều hành và tải lên checklist XLSX, rule JSON hoặc cả hai. File cũ được sao lưu với đuôi .bak.
+