AssetCore Audit — Module Readiness & Security
Skill này bao 2 nhiệm vụ: Module Audit (production-readiness) + Security Review.
Phần 1 — Module Audit (8-pillar checklist)
NGUYÊN TẮC BẤT BIẾN — UI Completeness
UC-1: Mọi module PHẢI có Create button
Mỗi list page phải có button tạo mới (không chỉ hiển thị danh sách). Kiểm tra:
- List view có "Tạo mới" / "+ New" / "+ [Tên bản ghi]" button
- Button gọi được modal hoặc navigate đến form mới
- Form tạo mới có đủ fields và submit được
Ngoại lệ duy nhất: các page chỉ đọc thuần túy (vd: audit trail, reports).
UC-2: Mọi bản ghi PHẢI có trang chi tiết với workflow actions
Mỗi bản ghi trong list phải:
- Có link/button "Chi tiết" hoặc click row dẫn đến URL chi tiết (vd:
/capas/:id) - Trang chi tiết hiển thị tất cả fields
- Trang chi tiết có workflow action buttons phù hợp với state
- State transitions phải khép kín (Draft → Approved → Active → Closed; không để bản ghi "kẹt" ở một state không có action)
Khi audit FE: navigate đến trang chi tiết của 1 bản ghi ở mỗi state → verify buttons.
UC-3: Asset detail — tất cả tabs phải có dữ liệu hoặc empty state rõ ràng
Trang /assets/:id có các tabs: Thông tin, Khấu hao, Lịch sử, KPI, Audit Trail. Mỗi tab phải:
- Hiển thị dữ liệu nếu có
- Hiển thị "Chưa có dữ liệu" rõ ràng nếu chưa có — không để trống hoàn toàn
- Widget Ngừng máy: hiển thị số liệu thực (0 nếu chưa có event, không blank)
UC-4: Tất cả Link fields phải hiển thị human-readable name
Các trường Link hiển thị cho user phải dùng display name, không phải DocType ID:
- Vendor/Supplier: tên công ty, không phải
SUP-2026-XXXXX - Asset: asset_name || asset_code, không phải
ACC-ASS-2026-XXXXX - User: full_name, không phải
email@domain.com - Department: tên khoa, không phải mã khoa
BE phải enrich *_name trong response; FE dùng x.xxx_name || x.xxx.
UC-5: Naming series PHẢI đúng format
DocType có naming series phải:
"naming_rule": "Naming Series"(không phải"Expression (old style)")"autoname": "PREFIX-.YYYY.-.#####"(không cóformat:prefix)- Verify bằng cách tạo bản ghi mới và check tên trả về — nếu trả về literal
"PREFIX-.YYYY.-.#####"thì sai
Mục đích
Dùng trước:
- Tag release (
v3.x.y) - Promote module Wave-Planned → Wave-Live
- Cut deployment ticket
- Đóng sprint deliver IMM-XX
Skill này chỉ verify — không implement. Khi phát hiện gap, chuyển sang assetcore-be, assetcore-fe, assetcore-test, assetcore-deploy.
8 pillars audit
Pillar 1 — DocType schema
-
module: "AssetCore"set -
autonamedùng prefix có ý nghĩa -
track_changes: 1 - Status fields:
read_only: 1+no_copy: 1 - Timestamp fields:
read_only: 1+no_copy: 1 - DocPerm đủ 2+ operational roles
- Không có field service dùng nhưng không có trong JSON
# Verify fields tồn tại
grep -n "doc\." services/immXX.py | grep -v "frappe\|get_doc\|db\." | head -20
Pillar 2 — Service layer
- 3-tier tách đúng (không có business logic trong API, không có HTTP trong service)
- Mọi mutating function có permission check ở đầu
-
require_role(...)dùng constant từRoles, không hardcode - Không có
except: passhayexcept Exception: pass - Không gọi
frappe.db.*trực tiếp từ service (đi qua repo)
# Tìm bare except
grep -n "except:" services/immXX.py | grep -v "ServiceError\|Exception as\|frappe"
# Tìm direct DB calls trong service
grep -n "frappe\.db\." services/immXX.py | grep -v "set_value\|commit"
Pillar 3 — Repository
-
<Name>Repo(BaseRepository)tồn tại - Không có raw SQL trừ khi thực sự cần join phức tạp
- Import từ
assetcore/repositories/__init__.py, không trực tiếp từ_repo.py
Pillar 4 — API layer
- Tất cả endpoints có
@frappe.whitelist() - Mutating endpoints có
methods=["POST"] - Function names khớp với
docs/imm-XX/05_API_Specification.md - Không có business logic trong API handlers
- Pagination params cast:
int(page),int(page_size) - Display name enrichment: mọi
list_*endpoint có Link field hiển thị → phải gọi_enrich(items, field, doctype, display_field)để thêm*_name; mọiget_*detail endpoint phải enrich tương tự (không chỉ list)
# Verify whitelist endpoints
grep -n "@frappe.whitelist" api/immXX.py
# Compare với spec
grep "endpoint\|POST\|GET" docs/imm-XX/05_API_Specification.md | head -20
# Tìm get_* endpoint thiếu enrich (trả frappe.get_doc().as_dict() nhưng có Link field user-visible)
grep -n "frappe.get_doc.*as_dict\|get_doc.*as_dict" api/immXX.py
Pillar 5 — Workflow
- Workflow JSON tồn tại trong
assetcore/assetcore/workflow/imm_XX_<name>_workflow.json -
name == workflow_nametrong JSON -
is_active: 1set - docstatus transitions valid (
0→0,0→1,1→1,1→2only) - Workflow trong
hooks.pyfixtures — đủ CẢ 3 lists (Workflow + State + Action) -
EXPECTED_WORKFLOWSupdated trongtests/test_workflows.py
# Verify 3 fixture lists — workflow name trong từng list phải match JSON
grep -A20 '"dt": "Workflow"' assetcore/hooks.py
grep -A50 '"dt": "Workflow State"' assetcore/hooks.py
grep -A30 '"dt": "Workflow Action Master"' assetcore/hooks.py
# Đếm states + transitions từ workflow JSON (không đoán)
python3 -c "import json; d=json.load(open('assetcore/assetcore/workflow/imm_XX_<name>_workflow.json')); print('states:', len(d['states']), 'transitions:', len(d['transitions']))"
# Verify tất cả state names trong hooks.py Workflow State list
python3 -c "import json; d=json.load(open('assetcore/assetcore/workflow/imm_XX_<name>_workflow.json')); [print(s['state']) for s in d['states']]"
Pillar 6 — FE (Frontend)
-
api/immXX.ts— all functions typedPromise<T>, khôngPromise<ApiResponse<T>> -
stores/immXX.ts— Pinia setup syntax; không re-export API namespace - Views: tri-branch
v-if="loading"/v-else-if="error"/v-else -
catch (e: unknown)+e instanceof Error ? e.message : String(e)— khôngcatch (e: any) - Routes đúng trong
router/index.tsvớimeta.moduleId - Launcher tile
disabled: false+ route tồn tại
FE Display Quality (bắt buộc kiểm tra):
- Display names, không phải system codes: mọi trường Link hiển thị cho user phải dùng human-readable name, không phải DocType id:
- Supplier/Vendor: dùng
supplier_name || supplier, không phảiSUP-2026-XXXXX - Asset: dùng
asset_name || asset, không phảiACC-ASS-2026-XXXXX - User: dùng
full_name || user, không phảiemail@domain - BE phải enrich
*_namevào response; FE dùng patternx.xxx_name || x.xxx
- Supplier/Vendor: dùng
- Status values FE = BE constants: grep
_STATUS_*trong service layer, verify mọiSTATUS_COLOR,STATUS_LABEL,allowed_transitions.includes(...),canXxx computeddùng ĐÚNG string đó- Lỗi hay gặp: FE dùng
"Under Investigation"nhưng BE constant là"In Progress"→ tất cả workflow buttons ẩn
- Lỗi hay gặp: FE dùng
-
allowed_transitions.includes()dùng exact BE string: lấy từ_VALID_TRANSITIONSdict, không đặt tên thân thiện - Select options FE = DocType JSON options: grep DocType JSON field
options, so với<select>options trong form — mismatch gây validation error -
useFormDraftcache: sau khi fix options, test với fresh browser session hoặc clear localStorage — draft cache giữ giá trị cũ không hợp lệ - Sidebar không che content: test viewport ≥ 1280px; sidebar fixed z-40 intercept clicks ở viewport nhỏ
UI Completeness (bắt buộc audit):
- List page có Create button: không có → 🟠 HIGH gap
- Detail page có workflow buttons: mỗi non-terminal state phải có ≥ 1 action button → không có → 🔴 CRITICAL (user bị kẹt)
- KPI/stats tabs có data thực: nếu có work orders nhưng uptime = 0/null → 🟠 HIGH (KPI service broken)
- Audit trail tab hiển thị events: empty khi có actions → 🟠 HIGH
- Tabs không empty giả: tất cả tabs phải fetch từ API, không hardcode empty
Procurement Plans — kiểm tra thêm:
-
/procurement-planslist page có Create button → tạo plan mới được - Detail page
/procurement-plans/:idcó đủ: tổng ngân sách, tỷ lệ sử dụng (allocated/budget), danh sách NR đã gắn vào plan - Tỷ lệ sử dụng không hiển thị 0% khi đã có NR gắn vào plan — nếu 0% → kiểm tra BE roll-up logic
- Workflow buttons đúng state: Draft → Submit → Approve → Active → Close
Asset Detail — kiểm tra thêm:
- Tab Thông tin: tất cả fields điền đầy đủ, vendor hiển thị tên công ty (không mã SUP-XXXX)
- Tab Khấu hao: hiển thị schedule nếu purchase_price ≠ 0; hiển thị "Chưa có dữ liệu" nếu chưa nhập giá
- Tab Lịch sử: ít nhất 1 lifecycle event sau khi asset được tạo/cài đặt
- Tab KPI: uptime%, MTBF, MTTR hiển thị số liệu hoặc "Chưa đủ dữ liệu để tính" — không để trống hoàn toàn
- Widget Ngừng máy: hiển thị "0 sự kiện ngừng máy" nếu chưa có downtime log (không blank, không error)
- Audit Trail tab: có ít nhất 1 entry từ lúc tạo asset — empty hoàn toàn → 🟠 HIGH
cd frontend && npx tsc --noEmit 2>&1 | head -30
# Grep để verify status strings:
grep -n "_STATUS_\|STATUS_COLOR\|STATUS_LABEL\|allowed_transitions" services/immXX.py views/immXX/*.vue
Pillar 7 — Tests
-
test_immXX.pytồn tại - Mỗi BR-XX-NN có ≥ 1 happy + 1 negative test
- Workflow smoke test pass
- Tests chạy được trên fresh site
bench --site miyano run-tests --module assetcore.tests.test_immXX
bench --site miyano run-tests --module assetcore.tests.test_workflows
Pillar 8 — Docs & Audit trail
-
docs/imm-XX/có đủ 9 files (README + 02→09) -
07_Testing_QA.mdcó bảng UAT scenarios - Mọi state transition gọi
log_audit_event(...)— không bypass - Không có module-local
_log_audithay_create_lifecycle_event(phải dùng canonical)
Realistic data check (dùng trong UAT, không chỉ unit test):
- Test data dùng tên thiết bị y tế thực, không phải "_Test", "sample"
- Work orders có complete fields: asset, technician, description thực tế
- KPI/stats được generate từ data thực (không mock 0)
- Audit trail có events thực sau khi tạo/sửa/chuyển trạng thái
Severity grading
- 🔴 Critical — app crashes, data corruption, security hole. Block release.
- 🟠 High — feature broken hoặc audit gap. Fix before Wave goes Live.
- 🟡 Medium — UX degraded, missing validation. Fix in next sprint.
- 🟢 Low — code smell, doc gap. Backlog.
Audit report format
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Module Audit — IMM-XX
Date: YYYY-MM-DD
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Pillar 1 DocType : ✅ / ❌ [gaps]
Pillar 2 Service : ✅ / ❌ [gaps]
Pillar 3 Repo : ✅ / ❌ [gaps]
Pillar 4 API : ✅ / ❌ [gaps]
Pillar 5 Workflow : ✅ / ❌ [gaps]
Pillar 6 FE : ✅ / ❌ [gaps]
Pillar 7 Tests : ✅ / ❌ [gaps]
Pillar 8 Docs/Audit: ✅ / ❌ [gaps]
VERDICT: ✅ PRODUCTION-READY / ❌ NOT READY
Critical gaps: [list]
Action items: [list với owner]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Phần 2 — Security Review
Threat model
- Privilege escalation — Technician trigger admin-only action
- Vendor data leakage — Hospital A thấy data Hospital B
- Audit trail tampering — backdating hoặc xóa lifecycle record
- Session hijacking — CSRF, stale token
- Injection — SQL via raw
frappe.db.sql, XSS in descriptions - Mass exfiltration — unbounded list endpoint dump toàn bộ table
Security checklist
Layer 1 — Service permission gate
from assetcore.services.shared.permissions import require_role
from assetcore.services.shared.constants import Roles
def assign_technician(name: str, *, technician: str):
require_role(Roles.CAN_CREATE_WO, "Không đủ quyền giao việc")
# ...
- Mọi mutating service function có
require_role(...)ở đầu - Roles từ
Rolesconstant, không hardcode string - Permission check TRƯỚC khi đọc record (không để data leak qua error message)
Layer 2 — DocPerm (defense in depth)
# Verify permissions trong JSON
grep -A10 '"permissions"' assetcore/assetcore/doctype/<name>/<name>.json
-
delete: 0cho mọi role trên audit trail DocTypes - Không có
System Managertrong non-admin DocType permissions -
read: 1tối thiểu cho operational roles
Whitelist hygiene
# Tìm endpoints thiếu permission gate
grep -B2 "@frappe.whitelist" api/immXX.py | grep -v "require_role\|#"
- Mọi POST endpoint có
methods=["POST"] - Mọi endpoint đọc data nhạy cảm có
require_rolehoặc filter theofrappe.session.user - Pagination params bounded:
min(int(page_size), 200)— không cho dump unlimited
Audit trail integrity
# Tìm bypass (insert trực tiếp thay vì log_audit_event)
grep -rn "doctype.*IMM Audit Trail" assetcore/ | grep -v "log_audit_event\|test_"
- Không có code insert
IMM Audit Trailtrực tiếp - Không có
frappe.delete_doc("IMM Audit Trail", ...)ngoài test teardown -
delete: 0trong DocPerm choIMM Audit Trail
Input validation & Injection
# Tìm raw SQL với string interpolation
grep -n "frappe\.db\.sql" assetcore/ -r | grep -v "?.*%s\|:%(.*)" | grep "%\|format\|f\""
- Raw SQL dùng parameterized queries (
%s, không f-string) - User-entered text không render as HTML (escape hoặc dùng Jinja
{{ value | e }}) - File upload qua
@frappe.whitelist(methods=["POST"])với MIME type check
Vendor isolation (multi-tenant)
# Mọi list query phải filter theo tenant
filters["hospital_site"] = frappe.local.site
# Hoặc check ownership:
if doc.created_by_hospital != frappe.local.site:
frappe.throw("Không có quyền truy cập")
- Mọi
list_*endpoint filter theohospital_sitehoặc user scope - Vendor Engineer không thấy data của hospital khác
Security report format
Security Review — IMM-XX / [endpoint/feature]
🔴 CRITICAL: [issue + exploit path + fix]
🟠 HIGH: [issue + fix]
🟡 MEDIUM: [issue + fix]
Verdict: SECURE / NEEDS FIX
Khi nào dùng skill nào tiếp theo
| Audit phát hiện | Skill tiếp |
|---|---|
| BE layer gap (service, repo, API) | assetcore-be |
| FE layer gap (views, store, types) | assetcore-fe |
| Test missing | assetcore-test |
| Deployment issue | assetcore-deploy |
| Doc gap | assetcore-doc |
Lessons Learned 2026-05 — Audit checklist mở rộng
Khi audit 1 module, bắt buộc check các pattern bug đã gặp:
A. Backend audit checks
# A1. Frappe 417 risk — int|None trong GET whitelist
grep -rn "int | None\|float | None" assetcore/api/ \
| xargs -I{} grep -B2 "@frappe.whitelist" {} 2>/dev/null
# A2. Schema mismatch — service ref field không có trong DocType
# Cho mỗi service file, list field assignments rồi cross-check với DocType JSON
grep -E "doc\.\w+ =" assetcore/services/<module>.py | sort -u
# A3. Workflow action label inconsistency
diff <(python3 -c "import json; d=json.load(open('workflow.json')); print(sorted(t['action'] for t in d['transitions']))") \
<(grep -E "transition.*action" assetcore/api/<module>.py)
# A4. Response enrichment — Link field phải có _name companion
grep -E "doctype.*Link" <doctype>.json
# Verify api/<module>.py có batch _enrich() cho từng Link field
# A5. Gate validator existence — mỗi gate G0X phải có function _validate_gate_g0X
grep -E "^def _validate_gate_g" assetcore/services/<module>.py
B. Frontend audit checks
# B1. TRANSITIONS_BY_STATE completeness — đếm states vs entries trong map
states=$(python3 -c "import json; d=json.load(open('workflow.json')); print(len(d['states']))")
entries=$(grep -c "':\\s*\\[" frontend/src/views/<module>/DetailView.vue)
echo "States: $states | Map entries: $entries" # phải bằng nhau (trừ terminal)
# B2. List page thiếu create button
for f in frontend/src/views/**/[A-Z]*ListView.vue; do
grep -L "Tạo\|+ \|create\|new" "$f"
done
# B3. Hardcoded internal codes trong template
grep -rn "AC-SUP-\|AC-DEPT-\|AC-ASSET-\|IMM-MDL-" frontend/src/views/ \
| grep -v "\.test\.\|\.spec\."
# B4. Link field as text input (bug pattern)
# Tìm <input type="text"> bind v-model có tên trùng Link field
grep -E "<input.*type=\"text\".*supplier|department|vendor|model" frontend/src/views/
# B5. StatusBadge sync — mỗi BE state có entry trong formatters
grep -E "^\s+'[A-Z][a-zA-Z\s]+':" assetcore/assetcore/workflow/*.json
grep "STATUS_LABEL\|STATUS_COLOR" frontend/src/utils/formatters.ts
C. UI audit checks (Playwright)
Cho mỗi page trong module:
1. browser_navigate → list page
2. browser_snapshot → grep "Tạo" || "+ " # phải có button create
3. browser_console_messages(error) # phải 0 errors
4. browser_evaluate: tìm regex /AC-(SUP|DEPT|ASSET)-\d+/g # phải 0 matches in user-facing text
5. browser_evaluate: tìm regex /\b[a-z0-9]{10}\b/g # phải 0 matches (Frappe auto-name leak)
6. Click row → detail page
7. browser_snapshot → count workflow buttons # phải >= 1 cho non-terminal state
8. Traverse all states → mỗi state phải có forward button
D. New audit verdict items
Thêm vào audit report:
== Frappe API hygiene ==
- [ ] No int|None params trong GET endpoints (LL-BE-1)
- [ ] All Link fields enriched với _name companion (LL-BE-2)
- [ ] Service code ref fields verified vs DocType JSON (LL-BE-3)
- [ ] All gate validators implemented as functions (LL-BE-5)
== FE-BE contract sync ==
- [ ] Workflow action labels match exact (LL-BE-4, LL-FE-2)
- [ ] TRANSITIONS_BY_STATE covers all states (LL-FE-1)
- [ ] StatusBadge sync với BE workflow states (LL-FE-3)
- [ ] Form Select options = DocType options (LL-FE-8)
- [ ] Form Link fields use dropdown (LL-FE-9)
== UI completeness ==
- [ ] Every list has create button (LL-FE-4)
- [ ] Every detail has workflow buttons for current state (LL-FE-5)
- [ ] No code/email leaks user-facing (LL-FE-6)
- [ ] No Frappe auto-name leaks (LL-FE-7)
E. Audit cross-reference
Khi audit bắt được pattern X → recommend fix theo skill tương ứng:
| Pattern phát hiện | Skill fix | Reference |
|---|---|---|
| 417 EXPECTATION FAILED | assetcore-be |
LL-BE-1 |
| Unknown column 1054 | assetcore-be |
LL-BE-3 |
| Workflow action 422 | assetcore-be + assetcore-fe |
LL-BE-4 + LL-FE-2 |
| Code/email leak UI | assetcore-be + assetcore-fe |
LL-BE-2 + LL-FE-6 |
| List thiếu create | assetcore-fe |
LL-FE-4 |
| State stuck | assetcore-fe |
LL-FE-1 |
| Auto-name leak | assetcore-fe |
LL-FE-7 |
| Form Link as text | assetcore-fe |
LL-FE-9 |
| Gate enforced muộn | assetcore-be |
LL-BE-5 |