Skip to content

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
Criticalactive security / data-loss / availability riskต้องแก้ก่อน deploy ถัดไป
Highcorrectness / maintainability hazard, real impactวางแผนแก้ใน quarter นี้
Mediumfriction / hygiene, ยังไม่อันตรายระดับ activeแก้ตามโอกาส
Lowcosmetic / typo / minor debtเก็บเป็นหมายเหตุ

Severity: Critical

SEC-PMS-01 · Endpoint คืนเงินเดือนพนักงานเป็น @Public() — ไม่ต้อง login

ที่: pms-api/src/modules/budget/budget.controller.ts (branch: uat):49-53GET /budget/getEmployeeSalary

Evidence:

typescript
@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:

bash
# ไม่ต้องมี 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:

  1. ลบ @Public() จาก getEmployeeSalary ทันที (และพิจารณา list / getOrgUnit ด้วย)
  2. ถ้าจำเป็นต้อง integrate ระบบภายนอก — ใส่ apiKey-only guard หรือ mTLS ไม่ใช่เปิดเปล่า
  3. ตรวจ access log ย้อนหลังว่ามี traffic ผิดปกติเข้า endpoint นี้หรือไม่
  4. ยืนยันกับทีม 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):41SECRET_KEY
  • pms-api/.env (branch: uat):45 — SMTP password
  • pms-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:

bash
git clone <pms-api-repo>
# อ่าน .env → ได้ DB password UAT ทั้ง 4 schema + SECRET_KEY + SMTP
# ใช้ SECRET_KEY เดียวกัน forge JWT ของ benefit/recruitment/ai-recruitment ที่เป็น prod

Impact:

  • UAT DB ของ PMS + workforce + vtrc เข้าถึงได้ถ้า network เปิด
  • Forge JWT ข้ามแพลตฟอร์ม เพราะ shared secret — ไม่ใช่แค่ risk ระดับ UAT
  • ส่งอีเมลในนาม noreply-vtrc@redcross.or.th ได้ถ้า SMTP ถูกขโมย
  • GCP SA ตามสิทธิ์ใน key file

Remediation:

  1. Rotate SECRET_KEY ทั้งแพลตฟอร์ม (ทุก service ที่แชร์) + DB passwords + SMTP + GCP key
  2. เพิ่ม .env และ *-sa-key.json เข้า .gitignore
  3. ล้าง git history (git filter-repo / BFG)
  4. ย้าย secret ไป secret manager (Vault / k8s Secret / cloud SM)
  5. แยก 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:

typescript
// 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:

  1. คืนเงื่อนไข targetDate && this.isSameDate(targetDate, today) ทันที
  2. เพิ่ม unit test สำหรับ date matching + advance notification
  3. ตรวจ tbNotify ย้อนหลังว่ามี duplicate รายวันหรือไม่ แล้วแจ้งผู้ใช้ถ้าจำเป็น
  4. พิจารณา 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:

typescript
@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:

  1. เพิ่ม in-process lock (if (this.running) return;)
  2. เพิ่ม distributed lock ถ้ามีหลาย replica (Redis SET NX หรือ SQL sp_getapplock)
  3. วัด duration และ alert เมื่อใกล้ 60s
  4. พิจารณาเปลี่ยนเป็น queue worker แยกจาก HTTP process

OBS-PMS-01 · Cron error handling แบบ silent — ไม่มี alert เมื่อ job ล้มเหลว

ที่:

  • configNotification.controller.ts:33-40catch { console.error(...) }
  • usagePermission.service.ts:1933-1935this.logger.error(error) อย่างเดียว
  • Job 1 ไม่ await promise ของ checkAndSendNotifications() (controller.ts:36)

Impact: SMTP ล่มหรือ workforce queue พังหลายวันโดยไม่มีใครในทีมรู้จนกว่าผู้ใช้ร้องเรียน; log "Daily notification check completed" อาจพิมพ์ก่อนงาน async จบจริง

Remediation:

  1. await this.emailService.checkAndSendNotifications()
  2. ส่ง alert (email/Slack/PagerDuty) เมื่อ catch
  3. health metric: last successful cron timestamp
  4. อย่าพึ่ง 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):19RUN 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

typescript
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):408POST 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

typescript
@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)

IDSeverityหัวข้อสั้นบทอ้างอิง
SEC-PMS-01CriticalPublic getEmployeeSalary04
SEC-PMS-02CriticalSecrets ใน git + shared JWT04
CORR-PMS-01HighNotification date gate ปิด05
CORR-PMS-02HighCron 1 นาที overlap05
OBS-PMS-01HighSilent cron errors / no await05
QUAL-PMS-01HighDocs แจ้งเตือนไม่ตรงโค้ด05
QUAL-PMS-02MediumDockerfile ไม่ frozen-lockfile01
OBS-PMS-02Mediumconsole.log + prod no-op01
QUAL-PMS-03Medium5 salary modules ชื่อคล้าย01
QUAL-PMS-04Mediumpms-ai-api ภายนอก04
SEC-PMS-03MediumPublic upload list04
ARCH-PMS-01Mediumworkforce DB coupling ×1003
QUAL-PMS-05LowTODO salaryAdjust
QUAL-PMS-06Lowtypo periodExtesnd filename03
CONF-PMS-01LowJWT expire/refresh env หายจาก .env04
DEAD-PMS-01Lowmigrate cron comment + empty AuthController05

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
3Rotate secrets ทั้งแพลตฟอร์ม + ลบออกจาก git history1–3 วัน (ประสานหลายทีม)SEC-PMS-02
4In-process + distributed lock ให้ cron 1 นาที0.5–2 วันCORR-PMS-02
5Alert เมื่อ cron fail + metric last-success1 วันOBS-PMS-01
6อัปเดต/ลบ PMS-Period-Notification-System.mdชั่วโมงQUAL-PMS-01
7yarn install --frozen-lockfile ใน DockerfileนาทีQUAL-PMS-02
8ยืนยัน DPA กับ pms-ai-api.vtrcsupport.comประสาน legalQUAL-PMS-04
9ลด workforce coupling (facade / HTTP)สัปดาห์–เดือนARCH-PMS-01
10เพิ่ม test ครอบ cron, salary adjust, budget public regressionสัปดาห์Test coverage เหลือง

สิ่งที่ทำได้ดีแล้ว (อย่าทำพังตอนแก้)

  1. *History audit trail สม่ำเสมอ ทั่วตารางหลัก — ดีกว่า benefit/recruitment
  2. Parameterized stored procedures (sp_get_employeeSalary_orgUnit, sp_GetSalaryPermission) — ไม่ใช่ string-interpolated SQLi
  3. ThrottlerGuard global — เกราะเดียวในกลุ่มที่ตั้งไว้
  4. NODE_ENV double-guard บน migrate / notification manual / promote email — ดีกว่า public batch ของ benefit-api
  5. ValidationPipe เข้ม (forbidNonWhitelisted: true)
  6. 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:

  1. GET /budget/getEmployeeSalary โดยไม่มี token → ต้องได้ 401 (หลังลบ @Public())
  2. วันที่ไม่ตรง settingGoalStart → cron แจ้งเตือนต้อง ไม่ ส่ง notiGoalStart
  3. วันตรง settingGoalStart (หรือ advance day) → ส่งได้ครั้งเดียวต่อวันต่อประเภท
  4. จำลอง getEmployeeTriggerQeue ช้า > 60s → job ที่สองต้อง skip (หลังมี lock)
  5. ปิด SMTP ชั่วคราว → ต้องมี alert/log ที่ ops มองเห็น ไม่ใช่แค่ console.error
  6. Login ปกติด้วย JWT + apiKey ยังผ่าน checkRoleRc ได้

ไป กลับสู่ index หรือ ดูภาพรวม Volume 6