Periksa variabel modul
Periksa variabel modul
Section titled “Periksa variabel modul”Variabel tak terdeklarasi, variabel mati, VM_SA, dan firewall.
Berkas: alat/hari-3/Praktik 11/tools/periksa-variabel.mjs
#!/usr/bin/env node// =====================================================================// Praktik 11 - periksa-variabel.mjs//// Tiga temuan yang tidak terdeteksi pemeriksa mana pun sebelumnya://// P11-6 SUBDOMAIN dipakai di sepuluh tempat tetapi tidak pernah// dideklarasikan di blok variabel Tahap 2// P11-7 DNS_ZONE_NAME dideklarasikan tetapi tidak pernah dipakai// P11-16 VM_SA memakai alamat "@example.com" yang bukan bentuk// service account Google//// Ketiganya bisa diperiksa tanpa GCP, cukup dari teks dokumen.//// Pakai:// node periksa-variabel.mjs <folder dokumen .docx> [--json]// =====================================================================
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';import { join, basename } from 'node:path';import { inflateRawSync } from 'node:zlib';
// Variabel yang seharusnya diturunkan dari PARTICIPANT_ID dan karena itu// wajib ada di blok variabel Tahap 2, bukan ditetapkan ulang di dokumen lain.const SEHARUSNYA_DIDEKLARASIKAN = ['SUBDOMAIN', 'VM_NAME', 'STATIC_IP_NAME', 'IMAGE_NAME'];
// Variabel yang boleh kosong di luar blok utama.const DIKECUALIKAN = new Set([ 'EMAIL', 'EXTERNAL_IP', 'PROJECT_NUMBER', 'GITHUB_USERNAME', 'GITHUB_REPOSITORY', 'NAMA_LAYER', 'ROLE', 'USER', 'UBUNTU_CODENAME', 'VERSION_CODENAME', 'GEOSERVER_PASSWORD', 'GEOSERVER_ADMIN_PASSWORD', 'NEXTJS_IMAGE', 'DBNAME', 'SUBDOMAIN', 'SLUG', 'VM']);
// Pola alamat service account Google yang sah.const POLA_SA_SAH = /^(?:\$\{[A-Z_]+\}|\d+)-(?:compute@developer\.gserviceaccount\.com|[a-z0-9-]+@[a-z0-9-]+\.iam\.gserviceaccount\.com)$/;
const argv = process.argv.slice(2);const keluaranJson = argv.includes('--json');const target = argv.find((a) => !a.startsWith('--')) ?? '.';
if (!existsSync(target)) { console.error(`Folder tidak ditemukan: ${target}`); process.exit(2);}
// ---------------------------------------------------------------- baca docx// Membaca teks dari .docx tanpa dependensi: berkas .docx adalah arsip ZIP,// dan isinya ada di word/document.xml.function bacaDocx(jalur) { let buf; try { buf = readFileSync(jalur); } catch { return ''; } if (buf.length < 4 || buf.readUInt32LE(0) !== 0x04034b50) return '';
let pos = 0; let xml = null; while (pos + 30 <= buf.length && buf.readUInt32LE(pos) === 0x04034b50) { const metode = buf.readUInt16LE(pos + 8); const ukuran = buf.readUInt32LE(pos + 18); const panjangNama = buf.readUInt16LE(pos + 26); const panjangExtra = buf.readUInt16LE(pos + 28); const nama = buf.toString('utf8', pos + 30, pos + 30 + panjangNama); const awalData = pos + 30 + panjangNama + panjangExtra;
if (nama === 'word/document.xml') { if (ukuran === 0) return ''; const data = buf.subarray(awalData, awalData + ukuran); try { xml = metode === 0 ? data.toString('utf8') : inflateRawSync(data).toString('utf8'); } catch { return ''; } break; } if (ukuran === 0) { if (nama.endsWith('/')) { pos = awalData; continue; } return ''; } pos = awalData + ukuran; } if (!xml) return '';
return xml .replace(/<\/w:p>/g, '\n') .replace(/<w:br\b[^>]*\/>/g, '\n') .replace(/<[^>]+>/g, '') .replace(/</g, '<') .replace(/>/g, '>') .replace(/&/g, '&') .replace(/"/g, '"') .replace(/'|'/g, "'");}
function kumpulkanDokumen(folder) { const hasil = []; for (const nama of readdirSync(folder)) { const jalur = join(folder, nama); if (statSync(jalur).isDirectory()) hasil.push(...kumpulkanDokumen(jalur)); else if (nama.endsWith('.docx')) hasil.push({ nama, isi: bacaDocx(jalur) }); } return hasil;}
const dokumen = kumpulkanDokumen(target).filter((d) => d.isi.length > 0);if (dokumen.length === 0) { console.error('Tidak ada dokumen .docx yang bisa dibaca di ' + target); process.exit(2);}
const temuan = [];const catat = (severity, judul, detail) => temuan.push({ severity, judul, detail });
// ---------------------------------------------------------------- 1. deklarasi// Blok variabel dikenali dari satu baris berisi PROJECT_ID="..." diikuti// baris-baris VAR="...". Ambil dari dokumen yang memuatnya.const semuaTeks = dokumen.map((d) => d.isi).join('\n');
const dideklarasikan = new Set();{ const blok = semuaTeks.match(/PROJECT_ID="[\s\S]{0,900}?VM_REGION="[^"]*"/); if (blok) { for (const m of blok[0].matchAll(/^\s*([A-Z_][A-Z0-9_]*)=/gm)) { dideklarasikan.add(m[1]); } }}
if (dideklarasikan.size === 0) { catat('sedang', 'Blok variabel Tahap 2 tidak terbaca', 'Pemeriksaan deklarasi dilewati. Pastikan dokumen memuat PROJECT_ID="..." dan VM_REGION="...".');}
const dipakai = new Set();for (const m of semuaTeks.matchAll(/\$\{?([A-Z_][A-Z0-9_]*)\}?/g)) dipakai.add(m[1]);
for (const v of SEHARUSNYA_DIDEKLARASIKAN) { if (dipakai.has(v) && !dideklarasikan.has(v)) { const jumlah = (semuaTeks.match(new RegExp(`\\$\\{${v}\\}|\\$${v}\\b`, 'g')) ?? []).length; catat('tinggi', `${v} dipakai ${jumlah} kali tetapi tidak dideklarasikan di blok Tahap 2`, 'Nilai itu ditetapkan ulang di dokumen lain, sehingga sesi Cloud Shell yang terputus\n' + 'akan menemukan variabel kosong dan perintahnya gagal tanpa menjelaskan sebabnya.'); }}
// ---------------------------------------------------------------- 2. variabel mati//// Dua tingkat pemakaian dibedakan:// - dipakai sebagai ${VAR} -> benar, nilainya benar-benar diambil// - disebut sebagai VAR -> hanya narasi, peserta membaca namanya//// Variabel yang hanya disebut tanpa pernah dipakai sebagai ${VAR} tetap// berguna (peserta tahu nama yang harus diisi di Console), jadi tidak// dilaporkan. Yang dilaporkan hanya yang tidak muncul sama sekali.for (const v of dideklarasikan) { if (DIKECUALIKAN.has(v)) continue; const sebagaiNilai = (semuaTeks.match(new RegExp(`\\$\\{${v}\\}|\\$${v}\\b`, 'g')) ?? []).length; const disebut = (semuaTeks.match(new RegExp(`(^|[^A-Z0-9_])_?${v}([^A-Z0-9_]|$)`, 'g')) ?? []).length; if (sebagaiNilai === 0 && disebut <= 1) { catat('sedang', `${v} dideklarasikan tetapi tidak pernah dipakai`, 'Tidak muncul lagi sebagai nilai maupun sebagai nama di seluruh dokumen.'); }}
// ---------------------------------------------------------------- 3. VM_SAfor (const m of semuaTeks.matchAll(/VM_SA="([^"]+)"/g)) { const nilai = m[1]; if (!POLA_SA_SAH.test(nilai)) { catat('tinggi', `VM_SA bukan alamat service account yang sah: ${nilai}`, 'Bentuk yang benar:\n' + ' ${PROJECT_NUMBER}-asisten01@example.com\n' + 'Perintah IAM yang memakainya akan gagal dengan pesan bahwa service account tidak ada.'); }}
// ---------------------------------------------------------------- 4. firewall// Modul hanya memeriksa firewall tanpa pernah membuatnya, dan tidak// menyebut siapa yang membuat. Diperiksa sebagai catatan, bukan kesalahan.const adaList = /firewall-rules list/.test(semuaTeks);const adaCreate = /firewall-rules create/.test(semuaTeks);if (adaList && !adaCreate) { catat('sedang', 'Firewall diperiksa tetapi tidak pernah dibuat di modul', 'Tidak ada perintah firewall-rules create di seluruh dokumen. Peserta yang melihat\n' + 'daftar kosong tidak tahu apakah firewall belum siap atau ada yang salah pada projectnya.');}
// ---------------------------------------------------------------- hasilif (keluaranJson) { console.log(JSON.stringify({ dokumen: dokumen.map((d) => d.nama), dideklarasikan: [...dideklarasikan].sort(), dipakai: [...dipakai].sort(), temuan }, null, 2)); process.exit(temuan.some((t) => ['kritis', 'tinggi'].includes(t.severity)) ? 1 : 0);}
const WARNA = { kritis: '\u001b[31m', tinggi: '\u001b[31m', sedang: '\u001b[33m', rendah: '\u001b[36m' };const URUT = { kritis: 0, tinggi: 1, sedang: 2, rendah: 3 };const R = '\u001b[0m';const w = (s, t) => (process.stdout.isTTY ? WARNA[s] + t + R : t);
console.log(`Folder : ${target}`);console.log(`Dokumen : ${dokumen.length} berkas (${dokumen.map((d) => basename(d.nama)).join(', ')})`);console.log(`Variabel dideklarasikan : ${dideklarasikan.size}`);console.log('');
if (temuan.length === 0) { console.log('\u001b[32mTidak ada temuan.\u001b[0m'); process.exit(0);}
console.log(`HASIL: ${temuan.length} temuan`);for (const t of [...temuan].sort((a, b) => URUT[a.severity] - URUT[b.severity])) { console.log(`${w(t.severity, '[' + t.severity.toUpperCase() + ']')} ${t.judul}`); if (t.detail) for (const b of String(t.detail).split('\n')) console.log(` ${b}`);}process.exit(temuan.some((t) => ['kritis', 'tinggi'].includes(t.severity)) ? 1 : 0);