6 · Scorecard + debt register
บทสรุปของ 01 ถึง 05 — severity catalog แบบเจาะลึกแต่ละ entry (exploit / impact / evidence / remediation) + health scorecard + modernization ROI
ทุก claim อ้างจาก canonical branch uat
Severity legend
| Level | เกณฑ์ | Action |
|---|---|---|
| Critical | active security / data-loss / availability risk | ต้องแก้ก่อน deploy ถัดไป |
| High | correctness / maintainability hazard, real impact | วางแผนแก้ใน quarter นี้ |
| Medium | friction / hygiene, ยังไม่อันตรายระดับ active | แก้ตามโอกาส |
| Low | cosmetic / typo / minor debt | เก็บเป็นหมายเหตุ |
Severity: Critical
SEC-PMS-01 · Endpoint คืนเงินเดือนพนักงานเป็น @Public() — ไม่ต้อง login
ที่: pms-api/src/modules/budget/budget.controller.ts (branch: uat):49-53 — GET /budget/getEmployeeSalary
Evidence:
@Public()
@Get('/getEmployeeSalary')
getEmployeeSalary(@Query() query: GetSalaryByOrgUnitDto) {
return this.service.getEmployeeSalary(query);
}ไม่มี JWT, ไม่มี apiKey, ไม่มี NODE_ENV double-guard (ต่างจาก migrate/notification ที่เป็น @Public() แต่มีชั้นสอง) — เข้าถึงได้ผ่าน global prefix ${BASE_PATH}api/v1 (= /pms-api/api/v1/... บน UAT)
backend เรียก sp_get_employeeSalary_orgUnit ที่ budget.repositories.ts:256-273 คืน salaryStaff, salaryEmployee, budgetStaff, budgetEmployee, budgetPerDepartment
Exploit:
# ไม่ต้องมี Authorization / apiKey
curl "https://uat-vtrcapi.redcross.or.th/pms-api/api/v1/budget/getEmployeeSalary?orgUnitCode=03&fiscalYear=2568"
# → JSON เงินเดือน/งบประมาณตามหน่วยงาน(ถูก throttle ที่ 100 req/60s ต่อ IP — ยัง scrape ได้ช้า ๆ)
Impact: เปิดเผยข้อมูลเงินเดือนรวม/งบประมาณระดับหน่วยงานโดยไม่ระบุตัวตน — ข้อมูล sensitive สูงสุดใน repo นี้ (เทียบเท่าหรือสูงกว่า PII ที่พบใน benefit-api) ผู้โจมตีบน network ที่ถึง UAT/prod สามารถสำรวจ org unit codes ได้จาก endpoint getOrgUnit ที่เป็น @Public() เช่นกัน (budget.controller.ts:38-41)
Remediation:
- ลบ
@Public()จากgetEmployeeSalaryทันที (และพิจารณาlist/getOrgUnitด้วย) - ถ้าจำเป็นต้อง integrate ระบบภายนอก — ใส่ apiKey-only guard หรือ mTLS ไม่ใช่เปิดเปล่า
- ตรวจ access log ย้อนหลังว่ามี traffic ผิดปกติเข้า endpoint นี้หรือไม่
- ยืนยันกับทีม frontend ว่าทำไมตั้งใจเปิด — หรือเป็นความผิดพลาดตอน debug
SEC-PMS-02 · Credential (UAT) + JWT secret ที่ share กับ prod ของ 3 service อื่น หลุดเข้า git
ที่:
pms-api/.env (branch: uat):12,19,26,33— MSSQL passwords (main / vtrc / workforce / workforceTemp)pms-api/.env (branch: uat):41—SECRET_KEYpms-api/.env (branch: uat):45— SMTP passwordpms-api/vtrc-gcp-sa-key.json (branch: uat)— GCP service account key ทั้งไฟล์pms-api/config/configuration.ts (branch: uat):69-211— device API keys hardcode
Evidence: .env มี NODE_ENV=uat และ host 10.188.128.58 commit ใน repo — SECRET_KEY ค่าเดียวกับที่ benefit-api / recruitment-api / ai-recruitment-api ใช้ (ยืนยันครั้งที่ 4 ในกลุ่ม Domain Microservices — ดู 04 Auth)
Exploit:
git clone <pms-api-repo>
# อ่าน .env → ได้ DB password UAT ทั้ง 4 schema + SECRET_KEY + SMTP
# ใช้ SECRET_KEY เดียวกัน forge JWT ของ benefit/recruitment/ai-recruitment ที่เป็น prodImpact:
- UAT DB ของ PMS + workforce + vtrc เข้าถึงได้ถ้า network เปิด
- Forge JWT ข้ามแพลตฟอร์ม เพราะ shared secret — ไม่ใช่แค่ risk ระดับ UAT
- ส่งอีเมลในนาม
noreply-vtrc@redcross.or.thได้ถ้า SMTP ถูกขโมย - GCP SA ตามสิทธิ์ใน key file
Remediation:
- Rotate
SECRET_KEYทั้งแพลตฟอร์ม (ทุก service ที่แชร์) + DB passwords + SMTP + GCP key - เพิ่ม
.envและ*-sa-key.jsonเข้า.gitignore - ล้าง git history (
git filter-repo/ BFG) - ย้าย secret ไป secret manager (Vault / k8s Secret / cloud SM)
- แยก JWT secret ต่อ service — เลิก share ข้าม domain
Severity: High
CORR-PMS-01 · Notification date gate ถูกปิด — อาจส่งอีเมลทุกประเภททุกวัน
ที่: pms-api/src/modules/configNotification/configNotificationSendEmail.service.ts (branch: uat):132-135
Evidence:
// Check if today is the notification date
if (process.env.NODE_ENV === 'development' || targetDate /*(targetDate && this.isSameDate(targetDate, today))*/) {
await this.sendPeriodNotificationEmail(notification, tbPeriod, emailTemplate, emailSubject);
}isSameDate(targetDate, today) ถูก comment ทิ้ง — นอก development เงื่อนไขเหลือแค่ targetDate ซึ่งเป็น Date object ที่ truthy เสมอเมื่อ period มีวันที่ตั้งไว้ → cron 06:00 จะเข้า sendPeriodNotificationEmail สำหรับ ทุก notiTypeCode ที่มีวันที่ ทุกวัน
Exploit / Trigger: ไม่ต้องโจมตี — เกิดเองทุกเช้าถ้า period ถูก confirm และ config isEmail=true
Impact: spam อีเมลแจ้งเตือนซ้ำทุกวันตลอดปีงบประมาณ; ผู้ใช้ ignore การแจ้งเตือนจริง; ภาระ SMTP; อาจชน rate limit ของ Outlook
Remediation:
- คืนเงื่อนไข
targetDate && this.isSameDate(targetDate, today)ทันที - เพิ่ม unit test สำหรับ date matching + advance notification
- ตรวจ
tbNotifyย้อนหลังว่ามี duplicate รายวันหรือไม่ แล้วแจ้งผู้ใช้ถ้าจำเป็น - พิจารณา idempotency key ต่อ
(notiTypeCode, fiscalYear, calendarDate)
CORR-PMS-02 · Cron sync ทุก 1 นาทีไม่มีการป้องกัน overlapping execution
ที่: pms-api/src/modules/usagePermission/usagePermission.service.ts (branch: uat):1928-1936
Evidence:
@Cron('*/1 * * * *')
async handleCron() {
try {
await this.getEmployeeTriggerQeue();
} catch (error) {
this.logger.error(error);
}
}getEmployeeTriggerQeue() (:1698+) ดึง queue จาก workforce → sort → dedupe → loop อัปเดต PMSUser/EvaluateUser/EvaluateForm + ส่งอีเมล — ไม่มี isRunning flag / mutex / DB lock
Impact: ถ้างานเกิน 1 นาที (promote/transfer จำนวนมาก) job ซ้อนกัน → race บน tbEmployeeTriggerQueue และข้อมูลพนักงานใน PMS; อีเมล promote ซ้ำได้
Remediation:
- เพิ่ม in-process lock (
if (this.running) return;) - เพิ่ม distributed lock ถ้ามีหลาย replica (Redis SET NX หรือ SQL sp_getapplock)
- วัด duration และ alert เมื่อใกล้ 60s
- พิจารณาเปลี่ยนเป็น queue worker แยกจาก HTTP process
OBS-PMS-01 · Cron error handling แบบ silent — ไม่มี alert เมื่อ job ล้มเหลว
ที่:
configNotification.controller.ts:33-40—catch { console.error(...) }usagePermission.service.ts:1933-1935—this.logger.error(error)อย่างเดียว- Job 1 ไม่
awaitpromise ของcheckAndSendNotifications()(controller.ts:36)
Impact: SMTP ล่มหรือ workforce queue พังหลายวันโดยไม่มีใครในทีมรู้จนกว่าผู้ใช้ร้องเรียน; log "Daily notification check completed" อาจพิมพ์ก่อนงาน async จบจริง
Remediation:
await this.emailService.checkAndSendNotifications()- ส่ง alert (email/Slack/PagerDuty) เมื่อ catch
- health metric: last successful cron timestamp
- อย่าพึ่ง
console.logใน prod (ถูก no-op อยู่แล้วที่main.ts:16-18)
QUAL-PMS-01 · เอกสารระบบแจ้งเตือนไม่ตรงกับโค้ดจริง
ที่: pms-api/docs/PMS-Period-Notification-System.md (branch: uat) เทียบกับ configNotification.controller.ts:30
Evidence: เอกสารบอกว่า cron อยู่ที่ period.controller.ts และถูก comment ต้อง uncomment — ทั้งสองข้อผิด (cron อยู่ที่ configNotification และ active แล้ว)
Impact: engineer ใหม่ปิด/เปิดผิดที่ หรือคิดว่าระบบแจ้งเตือนยังไม่ทำงาน ทั้งที่รันทุกวัน 06:00 — ซ้อนกับ CORR-PMS-01 ทำให้ debug ยากขึ้น
Remediation: อัปเดต docs ให้ชี้ไฟล์/บรรทัดจริง หรือลบ docs เก่าแล้วลิงก์มาที่ Volume นี้
Severity: Medium
QUAL-PMS-02 · Dockerfile build โดยไม่ล็อค lockfile
ที่: pms-api/Dockerfile (branch: uat):19 — RUN yarn install (ไม่มี --frozen-lockfile)
เทียบ benefit-api/Dockerfile ที่มี --frozen-lockfile
Impact: build ได้ dependency version ต่างจากที่ทดสอบถ้า yarn.lock ไม่ sync กับ package.json
Remediation: เปลี่ยนเป็น yarn install --frozen-lockfile
OBS-PMS-02 · console.log กระจายทั่ว codebase — ปิดแบบ no-op ใน prod
ที่: pms-api/src/main.ts (branch: uat):16-18
if (process.env.NODE_ENV === 'prod') {
console.log = () => {};
}grep console.log ใน src/ พบกระจายมากกว่า 90 จุดข้ามหลาย module (มากกว่า benefit + recruitment รวมกันในรอบสำรวจก่อนหน้า)
Impact: วิธีปิดที่ปลายทางทำให้ debug ใน production ยาก — log ที่มีประโยชน์หายไปด้วย ไม่ใช่แค่ noise; ไม่ได้บังคับให้ใช้ I3GatewayLogger
Remediation: แทนที่ console.log ด้วย logger เป็นระยะ; ลบ no-op hack เมื่อ migration เสร็จ
QUAL-PMS-03 · Module ปรับเงินเดือน 5 ตัวชื่อคล้ายกันมาก
ที่: modules salaryRange, salaryPercentage, salaryAdjust, salaryAdjustment, salaryAdjustToHRMI (ดู 01 Repository map)
Impact: engineer ใหม่แก้ผิดโมดูล — โดยเฉพาะ salaryAdjust vs salaryAdjustment
Remediation: เพิ่ม README สั้นใน docs/ หรือแผนภาพใน Volume นี้; พิจารณา rename ใน major version
QUAL-PMS-04 · Third-party AI service บน domain ภายนอกรับข้อมูลคอร์ส/empCode
ที่: pms-api/src/modules/createForm/createForm.service.ts (branch: uat):408 — POST https://pms-ai-api.vtrcsupport.com/vector-embeddings/createMasterCourse
Impact: ข้อมูลชื่อคอร์ส + createdBy (empCode) ออกนอกโดเมน redcross.or.th — ต้องมี data processing agreement / สัญญา vendor
Remediation: ยืนยันกับ security/legal; ใส่ network allowlist; พิจารณา self-host หรือย้ายเข้าโดเมนองค์กร
SEC-PMS-03 · @Public() อ่านรายการไฟล์อัปโหลดตาม id
ที่: pms-api/src/modules/uploadFiles/upload.controller.ts (branch: uat):13-16
@Public()
@Get(':id')
getFilesList(@Param() param: UploadByIdDto) {
return this.service.getFilesList(param);
}Impact: ถ้า id เดาได้หรือรั่วจากที่อื่น อาจอ่าน metadata/ไฟล์โดยไม่ login (ขึ้นกับว่า service คืนอะไร — ต้องตรวจเพิ่ม)
Remediation: ลบ @Public() หรือใส่ ownership check + auth
ARCH-PMS-01 · Coupling ตรงกับ dbWorkforce จาก 10 module
ที่: grep @InjectEntityManager('workforce') — 10 module (ดู 03 Persistence)
Impact: schema change ที่ workforce กระทบ pms-api หลายจุดพร้อมกัน; ทดสอบ integration ยาก; ไม่มี anti-corruption layer
Remediation: ค่อย ๆ ย้ายไป workforce-api HTTP หรือ shared read model; อย่างน้อยรวม query ไว้ repository ชั้นเดียว
Severity: Low
QUAL-PMS-05 · TODO ค้าง — business logic ยังไม่ยืนยัน
ที่: pms-api/src/modules/salaryAdjust/salaryAdjust.service.ts (branch: uat):127 — // TODO: ตรวจสอบ business logic เพิ่มเติม
Impact: จุดปรับเงินเดือนมีความไม่แน่ใจค้างในโค้ด production path
Remediation: ปิด TODO ด้วย test + ยืนยันจากธุรกิจ หรือเปิด ticket ชัดเจน
QUAL-PMS-06 · Typo ชื่อไฟล์ entity (periodExtesnd)
ที่: pms-api/src/entities/periodExtesnd.entity.ts (branch: uat) — class/table ถูกต้อง (PeriodExtendEntity / tbPeriodExtend) แต่ชื่อไฟล์สะกดผิด
Impact: ค้นหาไฟล์ผิดชื่อ; ไม่กระทบ runtime
Remediation: rename ไฟล์ + อัปเดต import (low risk)
CONF-PMS-01 · JWT_EXPIRE_IN / REFRESH_TOKEN_SECRET_KEY ไม่พบใน .env ที่ commit
ที่: auth.service.ts:21-30 อ่าน env เหล่านี้ แต่ .env บน branch uat มีแค่ SECRET_KEY
Impact: ขึ้นกับค่าที่ inject ตอน runtime นอก repo — ถ้าหาย refresh token อาจ sign ด้วย undefined secret (UNVERIFIED ค่าบน server จริง)
Remediation: ใส่ใน secret manager + .env.example (ไม่ commit ค่าจริง); fail-fast ตอน boot ถ้าขาด
DEAD-PMS-01 · Migrate cron ถูก comment + AuthController ว่าง
ที่: migrateData.controller.ts:15-29, auth.controller.ts (ไม่มี route)
Impact: dead code สับสน; auth flow ไม่ชัดจาก controller นี้
Remediation: ลบหรือ document ชัดว่า entry จริงอยู่ที่ไหน
pms-api Health Scorecard
- สถานะแดง — มีปัญหาจริง ควรแก้ก่อนขยายฟีเจอร์
- สถานะเหลือง — ใช้ได้ แต่มีจุดอ่อน ต้องระวังตอนแก้
- สถานะเขียว — ผ่านเกณฑ์ในมิตินี้
| Dimension | สถานะ | หมายเหตุ |
|---|---|---|
| Auth pipeline | 🟡 | มี throttle + apiKey + checkRole แต่มี public salary endpoint ร้ายแรง |
| Persistence & transaction | 🟢 | query builder + parameterized SP เป็นหลัก; *History audit สม่ำเสมอ; ไม่พบ SQLi แบบ benefit-api |
| Secrets management | 🔴 | .env UAT + GCP key ใน git; SECRET_KEY แชร์กับ prod ของ 3 service |
| External integration resilience | 🟡 | workforce coupling 10 module; AI บน vtrcsupport.com; checkRole เป็น single point of 403 |
| Code duplication | 🟡 | 5 module salary ชื่อคล้าย; service เดี่ยวยาว ~3k บรรทัด |
| Background jobs | 🔴 | date-gate bug แจ้งเตือน + ไม่มี overlap lock + silent errors |
| Logging & observability | 🟡 | console.log หนาแน่น; no-op ใน prod; ไม่มี cron metrics/alert |
| Layering (controller/service/entity) | 🟡 | NestJS แยกโมดูลชัด แต่ usagePermission/createForm ใหญ่เกิน |
| Test coverage | 🟡 | 14 spec จาก ~33 module — ครอบคลุมน้อยกว่าครึ่ง; ไม่ครอบ cron/salary path สำคัญ |
| Config & secrets | 🔴 | ซ้ำ secrets management + device keys hardcode ใน configuration.ts |
คะแนนรวม: 1 เขียว / 6 เหลือง / 3 แดง
Finding index (quick reference)
| ID | Severity | หัวข้อสั้น | บทอ้างอิง |
|---|---|---|---|
| SEC-PMS-01 | Critical | Public getEmployeeSalary | 04 |
| SEC-PMS-02 | Critical | Secrets ใน git + shared JWT | 04 |
| CORR-PMS-01 | High | Notification date gate ปิด | 05 |
| CORR-PMS-02 | High | Cron 1 นาที overlap | 05 |
| OBS-PMS-01 | High | Silent cron errors / no await | 05 |
| QUAL-PMS-01 | High | Docs แจ้งเตือนไม่ตรงโค้ด | 05 |
| QUAL-PMS-02 | Medium | Dockerfile ไม่ frozen-lockfile | 01 |
| OBS-PMS-02 | Medium | console.log + prod no-op | 01 |
| QUAL-PMS-03 | Medium | 5 salary modules ชื่อคล้าย | 01 |
| QUAL-PMS-04 | Medium | pms-ai-api ภายนอก | 04 |
| SEC-PMS-03 | Medium | Public upload list | 04 |
| ARCH-PMS-01 | Medium | workforce DB coupling ×10 | 03 |
| QUAL-PMS-05 | Low | TODO salaryAdjust | — |
| QUAL-PMS-06 | Low | typo periodExtesnd filename | 03 |
| CONF-PMS-01 | Low | JWT expire/refresh env หายจาก .env | 04 |
| DEAD-PMS-01 | Low | migrate cron comment + empty AuthController | 05 |
Modernization — ลำดับ ROI
เรียงจาก impact สูง / effort ต่ำ ไปสูง
| ลำดับ | งาน | Effort โดยประมาณ | ปิด finding |
|---|---|---|---|
| 1 | ลบ @Public() จาก getEmployeeSalary (+ ตรวจ list/getOrgUnit) | ชั่วโมง | SEC-PMS-01 |
| 2 | คืน isSameDate gate ใน notification cron + await | ชั่วโมง | CORR-PMS-01, ส่วน OBS-PMS-01 |
| 3 | Rotate secrets ทั้งแพลตฟอร์ม + ลบออกจาก git history | 1–3 วัน (ประสานหลายทีม) | SEC-PMS-02 |
| 4 | In-process + distributed lock ให้ cron 1 นาที | 0.5–2 วัน | CORR-PMS-02 |
| 5 | Alert เมื่อ cron fail + metric last-success | 1 วัน | OBS-PMS-01 |
| 6 | อัปเดต/ลบ PMS-Period-Notification-System.md | ชั่วโมง | QUAL-PMS-01 |
| 7 | yarn install --frozen-lockfile ใน Dockerfile | นาที | QUAL-PMS-02 |
| 8 | ยืนยัน DPA กับ pms-ai-api.vtrcsupport.com | ประสาน legal | QUAL-PMS-04 |
| 9 | ลด workforce coupling (facade / HTTP) | สัปดาห์–เดือน | ARCH-PMS-01 |
| 10 | เพิ่ม test ครอบ cron, salary adjust, budget public regression | สัปดาห์ | Test coverage เหลือง |
สิ่งที่ทำได้ดีแล้ว (อย่าทำพังตอนแก้)
*Historyaudit trail สม่ำเสมอ ทั่วตารางหลัก — ดีกว่า benefit/recruitment- Parameterized stored procedures (
sp_get_employeeSalary_orgUnit,sp_GetSalaryPermission) — ไม่ใช่ string-interpolated SQLi - ThrottlerGuard global — เกราะเดียวในกลุ่มที่ตั้งไว้
NODE_ENVdouble-guard บน migrate / notification manual / promote email — ดีกว่า public batch ของ benefit-api- ValidationPipe เข้ม (
forbidNonWhitelisted: true) - Period cycle แยกสถานะด้วยวันที่ — ดู 02 Period cycle
สิ่งที่ยัง UNVERIFIED ในรอบนี้
| หัวข้อ | ทำไมยังไม่ยืนยัน |
|---|---|
| จำนวน replica จริงตอน deploy | ไม่มีไฟล์ k8s/helm ใน repo นี้ |
mDropdown แชร์ DB กับ benefit หรือไม่ | คนละ schema name (pms vs benefit) แต่ชื่อตารางเหมือน |
การใช้ runtime ของ VTRC_SERVER / connection vtrc | ประกาศไว้ ไม่พบ inject/call |
ค่า JWT_EXPIRE_IN / REFRESH_* บน server | ไม่มีใน .env ที่ commit |
| ความสัมพันธ์ workflow-api ↔ pms-ai-api | มี docs ใน workflow-api ยังไม่เปิด verify |
ว่า getEmployeeSalary ถูกเรียกจากระบบภายนอกโดยตั้งใจหรือไม่ | ไม่พบ caller ใน frontend ของ workspace นี้ในรอบสำรวจ |
Regression checklist หลังแก้ Critical/High
ใช้เป็น test plan ขั้นต่ำหลัง patch:
GET /budget/getEmployeeSalaryโดยไม่มี token → ต้องได้401(หลังลบ@Public())- วันที่ไม่ตรง
settingGoalStart→ cron แจ้งเตือนต้อง ไม่ ส่งnotiGoalStart - วันตรง
settingGoalStart(หรือ advance day) → ส่งได้ครั้งเดียวต่อวันต่อประเภท - จำลอง
getEmployeeTriggerQeueช้า > 60s → job ที่สองต้อง skip (หลังมี lock) - ปิด SMTP ชั่วคราว → ต้องมี alert/log ที่ ops มองเห็น ไม่ใช่แค่
console.error - Login ปกติด้วย JWT + apiKey ยังผ่าน
checkRoleRcได้
ไป กลับสู่ index หรือ ดูภาพรวม Volume 6