6 · Scorecard + debt register
บทสรุปจาก 01–05 สำหรับ ai-recruitment-api (branch: prod) — severity catalog พร้อม evidence / impact / remediation + health scorecard
Severity legend
| Level | เกณฑ์ | Action |
|---|---|---|
| 🔴 Critical | active security / financial / data-loss risk | แก้ก่อนปล่อยรอบถัดไป |
| 🟡 High | correctness / abuse / maintainability ที่กระทบจริง | วางแผนใน sprint ใกล้ ๆ |
| 🟢 Medium | friction / hygiene | แก้ตามโอกาส |
Severity: Critical (🔴)
SEC-AIRECRUIT-01 · Production secrets + billable API keys อยู่ใน git
ที่:
ai-recruitment-api/.env (branch: prod):2,5,7,9-10,11- ถูก track (
git ls-files .env) ทั้งที่.gitignore:51มี.env
Evidence: ไฟล์มี DB_PASSWORD, OPENAI_API_KEY, REMOVE_BG_API_KEY, HCTI_*, JWT_SECRET เป็น plaintext
OpenAI / remove.bg เป็นคีย์ที่เรียกแล้วเกิดค่าใช้จ่ายจริง
Impact:
- ใคร clone repo (หรืออ่าน history) ได้คีย์ → ใช้โควต้าองค์กรได้จนกว่าจะ rotate
- JWT secret ที่รั่ว = ออก token ปลอมเข้าทุก endpoint ของ service นี้ (และ service ที่ share secret — ดูด้านล่าง)
- MSSQL credential ถ้า network ถึง host ได้ = อ่านข้อมูล recruitment AI schema
Remediation:
- Rotate OpenAI, remove.bg, HCTI, JWT, MSSQL ทันที
- ลบ
.envออกจาก git index + ล้าง history (git filter-repo/ BFG) - ใช้ secret manager / k8s Secret / CI env ตอน deploy
- ตรวจ Dockerfile
COPY . .ไม่ให้ bake secret เข้า image
SEC-AIRECRUIT-02 · Auth ตรวจแค่ JWT signature — ไม่มี role / issuer / client check
ที่: middleware/verifyJWT.js:7-39 + mount server.js:37
Evidence:
const decoded = jwt.verify(token, SECRET_KEY);
req.user = decoded;
next();ไม่มีการอ่าน decoded.role, decoded.client, decoded.iss
Shared secret confirmed: JWT_SECRET ของ service นี้ hash ตรงกับ SECRET_KEY ของ benefit-api และ recruitment-api (ดู 04)
Impact:
- Token ที่ออกให้ context อื่น (รวมถึง client ที่ไม่ใช่ back-office ถ้าใช้ secret เดียวกัน) ใช้เรียก generate ภาพ / สัมภาษณ์ / OpenAI ได้
- ไม่มี quota ต่อ uid — abuse = บิล OpenAI พุ่ง
Remediation:
- ตรวจ claim ที่ issuer ของ back-office ใส่ไว้ (เช่น
client,iss) ให้ allowlist - แยก signing key ของ AI helper จาก candidate-facing tokens ถ้าเป็นไปได้
- ใส่ rate limit ต่อ
req.user.uid/ IP ที่ reverse proxy
Severity: High (🟡)
QUAL-AIRECRUIT-01 · Dead code / broken export ทำให้เข้าใจผิดว่า endpoint มีหรือไม่มี
ที่:
controllers/module2_1_get_background_v2.js— exportgetBackgroundV2ไม่ถูก mountcontrollers/module_2_3_combine_image.js— ไม่ถูก requireroutes/get-post-upload_image.js— ไม่ถูก mount จากserver.jscontrollers/get-post-upload_image.js:176exportremoveBgUploadControllerแต่routes/index.js:33ต้องการuploadImageController→undefined(log ที่:44)
Impact: ทีมอาจแก้ไฟล์ผิดตัว (เช่น แก้ module_2_3_* แล้ว production ไม่เปลี่ยน) หรือคิดว่า upload route พังทั้งที่ route จริงใช้ removeBgUploadController.js
Remediation: ลบหรือ mount ให้ชัด; แก้ export ให้ตรงชื่อถ้ายังต้องการไฟล์สำเนา
CORR-AIRECRUIT-01 · Sync jobReqPosition ระหว่าง recruitment กับ recruitment_ai ไม่ปรากฏใน repo นี้
ที่: 03 Persistence — service อ่านอย่างเดียวจาก recruitment_ai
ไม่มี cron (05) ที่ copy ข้อมูล
Impact: back-office เห็นตำแหน่งในระบบหลักแต่ AI description ว่าง เพราะแถวใน AI DB ไม่ตรง / ไม่มี
Remediation: เอกสาร + ยืนยันกับทีม data ว่า ETL อยู่ที่ใด; หรือเพิ่ม sync ที่ ownership ชัดใน recruitment-api
CORR-AIRECRUIT-02 · Prompt "technical" ของ interview อาจคัดลอกผิดเป็นข้อความ General
ที่: generateInterviewQuestionsController.js:158-169
ตัวแปร technicalQuestion แต่ prompt ยังกล่าวว่า "writing 10 General interview question" และพูดถึง soft skills เหมือน general (:167)
Impact: ชุด Technical กับ General อาจซ้ำเชิงเนื้อหา — คุณภาพคำถามสัมภาษณ์เพี้ยนโดยไม่มี error ชัด
Remediation: แก้ข้อความ prompt technical ให้ตรงเจตนา; เพิ่ม regression test ตรวจ substring
PERF-AIRECRUIT-01 · Puppeteer launch ต่อ request + OpenAI fan-out หนา
ที่:
combineJobPostingController.js:377-386launch ใหม่ทุกครั้ง- Job description 4× gpt-4o sequential (
:230-334) - Character path 3× image + remove.bg (
module2_2_controller.js:174-201) - Interview ≥6× gpt-3.5 (
:156-291)
Impact: latency สูง, ต้นทุนต่อคลิกสูง, risk OOM บน replica เล็กเมื่อมีคนกดพร้อมกัน
Remediation: browser pool / reuse; cache ผลลัพธ์; ลดจำนวน image; queue สำหรับ combine
Severity: Medium (🟢)
QUAL-AIRECRUIT-02 · Hardcode สมมติฐานประชากรใน PROMPT_RULES
ที่: module2_2_controller.js:31-44
แทนอาชีพบางกลุ่มด้วย "asian age 40 ..." เสมอ
Impact: ภาพประกาศสาธารณะสะท้อน bias ที่แก้ได้แค่ผ่านโค้ด; ไม่มี config
Remediation: ทบทวนกับ HR/comms; ย้าย rules ไป config ที่อนุมัติได้
OBS-AIRECRUIT-01 · ไม่มี test suite
ที่: ไม่พบไฟล์ .test.js / .spec.js ทั้ง repo
Impact: เปลี่ยน prompt / SQL / route แล้วไม่มี safety net — regression สูงสุดในกลุ่มที่เทียบกับ recruitment-api (มี spec จำนวนมาก)
OBS-AIRECRUIT-02 · Logger สองระบบปนกัน
ที่: มี logger.js แต่ controllers/services ใช้ console.log/console.error จำนวนมาก (เช่น routes/index.js:26,41-48, verifyJWT.js:27, removeBgService.js:189)
Impact: รวม log ใน aggregator ยาก; format ไม่สม่ำเสมอ
QUAL-AIRECRUIT-03 · Dependencies ประกาศแต่ไม่ใช้ / ขาดจาก package.json
| รายการ | หลักฐาน |
|---|---|
canvas, sharp ใน dependencies | package.json:12,22 — ไม่พบ `require('canvas' |
uuid ถูก require | module2_1_get_background.js:4 — ไม่อยู่ใน dependencies ของ package.json |
HCTI_*, DALLE_ENDPOINT, GEN_BASE_URL | ใน .env แต่ไม่มี caller ใน .js |
Impact: Docker ยัง build native libs ของ canvas โดยไม่จำเป็น; uuid อาจพังบน install ใหม่ถ้าไม่มี transitive
QUAL-AIRECRUIT-04 · saveFileToAPI hardcode ชื่อไฟล์
ที่: uploadController.js:6-7 — fileName = 'doctor', folder = 'Module2_2' เสมอ
Impact: ไฟล์ที่เซฟชนกันเชิงความหมาย; ไม่สะท้อนชนิดงานจริง
LEG-AIRECRUIT-01 · Node 18 base image
ที่: Dockerfile:1 — FROM node:18-alpine
sibling services ใช้ Node 20
Impact: ติดตาม EOL / CVE แยกเวอร์ชัน; ไม่สม่ำเสมอใน platform group
QUAL-AIRECRUIT-05 · Binary assets จำนวนมากใน git
ที่: storage-uploads/ ≈ 361 ไฟล์ (Background 29 + คลังตัวละคร)
Impact: clone ช้า, diff รก; ไม่ใช่ pattern object storage
SEC-AIRECRUIT-03 · Upload CORS ตั้ง origin เป็น * ได้
ที่: routes/index.js:91-106 — Access-Control-Allow-Origin: req.headers.origin || '*'
ขณะที่ global CORS จำกัด 3 origins (server.js:24-28)
Impact: นโยบาย CORS ไม่สม่ำเสมอ; browser จาก origin อื่นอาจ preflight ผ่านบน path นี้ (ยังต้องมี JWT)
ai-recruitment-api Health Scorecard
- 🔴 แดง — มีปัญหาจริง ควรแก้
- 🟡 เหลือง — ใช้ได้ แต่มีจุดอ่อน
- 🟢 เขียว — ผ่านเกณฑ์สำหรับมิตินี้
| Dimension | สถานะ | หมายเหตุ |
|---|---|---|
| Auth pipeline | 🔴 | verify signature อย่างเดียว; shared secret ข้าม 3 services; ไม่มี role |
| Persistence & SQL safety | 🟢 | parameterized queries; read-only; scope ตารางชัด |
| Secrets management | 🔴 | .env tracked รวม billable API keys |
| External integration resilience | 🟡 | fail-soft บางจุดดี; ไม่มี timeout/retry ชัดบน OpenAI; Puppeteer ต่อ request |
| Code duplication / dead code | 🔴 | orphan controllers/routes + export ชื่อไม่ตรง |
| Background jobs | 🟢 | ไม่มี cron — ไม่มีความเสี่ยงกลุ่มนี้ |
| Logging & observability | 🟡 | มี logger.js แต่ปน console.* |
| Layering | 🟡 | แยก routes/controllers/services พอใช้; validation กระจาย; ไม่มี DTO layer |
| Test coverage | 🔴 | 0 test files |
| Prompt / content governance | 🟡 | PROMPT_RULES hardcode; technical prompt น่าสงสัย |
คะแนนรวม: 2 🟢 / 4 🟡 / 4 🔴
Modernization — ลำดับ ROI
- Rotate + ถอด secrets ออกจาก git (SEC-AIRECRUIT-01) — รวม OpenAI key
- เพิ่ม authorization claims + rate limit (SEC-AIRECRUIT-02)
- ลบ/mount dead code ให้ตรงความจริง (QUAL-AIRECRUIT-01)
- ยืนยัน sync
jobReqPositionกับทีม data (CORR-AIRECRUIT-01) - Browser pool + ลด fan-out AI (PERF-AIRECRUIT-01)
- Smoke tests สำหรับ route สำคัญ + mock OpenAI (OBS-AIRECRUIT-01)
- ย้าย
storage-uploadsไป object storage (QUAL-AIRECRUIT-05) - อัป Node 20 ให้สอดคล้อง sibling services (LEG-AIRECRUIT-01)
ตาราง debt ID ด่วน
| ID | Severity | หัวข้อสั้น |
|---|---|---|
| SEC-AIRECRUIT-01 | 🔴 | secrets in git |
| SEC-AIRECRUIT-02 | 🔴 | no-role JWT / shared secret |
| SEC-AIRECRUIT-03 | 🟢 | upload CORS * |
| QUAL-AIRECRUIT-01 | 🟡 | dead code / bad export |
| QUAL-AIRECRUIT-02 | 🟢 | PROMPT_RULES ethnicity/age |
| QUAL-AIRECRUIT-03 | 🟢 | unused / missing deps |
| QUAL-AIRECRUIT-04 | 🟢 | hardcode save filename |
| QUAL-AIRECRUIT-05 | 🟢 | binaries in git |
| CORR-AIRECRUIT-01 | 🟡 | AI DB sync unknown |
| CORR-AIRECRUIT-02 | 🟡 | technical prompt copy-paste |
| PERF-AIRECRUIT-01 | 🟡 | puppeteer + AI fan-out |
| OBS-AIRECRUIT-01 | 🟢 | no tests |
| OBS-AIRECRUIT-02 | 🟢 | console vs logger |
| LEG-AIRECRUIT-01 | 🟢 | Node 18 |
สิ่งที่ยืนยันแล้ว vs สมมติ
ยืนยัน
- Express + global JWT no role
- OpenAI / remove.bg / QR / Puppeteer call sites ทั้งหมดในตารางบท 02
- Shared JWT hash กับ benefit + recruitment
- Shared DB name/host hash กับ recruitment
DB_AI_* - ไม่มี cron
ไม่ยืนยัน
- ลำดับคลิกจริงใน UI back-office
- ว่า production rotate คีย์ไปแล้วหรือยังหลัง commit
.env - เจ้าของ ETL ของ
recruitment_ai