Server yang baru mendapat alamat IP publik tidak menunggu lama untuk dikunjungi: dalam hitungan menit pertama, bot pemindai sudah mencoba SSH di port 22, HTTP di port 80, dan segala pintu yang pernah dipakai perangkat lain. Sebagian besar kunjungan itu tidak berbahaya, tetapi cukup satu service usang yang terlupa dimatikan — database uji coba, panel admin, bahkan server dev lama — untuk menjadi pintu masuk. Proyek ini mengamankan server publik secara metodis lewat tiga fase: audit apa yang sedang terbuka, firewall yang menutup semuanya kecuali yang kita izinkan, lalu verifikasi yang membuktikan aturan itu benar-benar bekerja — bukan sekadar terpasang.

Setiap fase menghasilkan artefak yang bisa dibaca ulang:

  • audit-baseline.sh — mengumpulkan service berjalan, port terbuka, user dengan shell, kunci SSH per user, dan paket tertinggal menjadi satu laporan bernomor beserta ringkasan temuan,
  • latihan.nft — ruleset nftables default-deny: chain input berkebijakan drop, loopback dan balasan koneksi sah diloloskan, hanya SSH/HTTP/HTTPS yang dibuka, sisanya dicatat dengan rem lalu dibuang,
  • validasi-ruleset.sh — sepuluh pemeriksaan struktur dengan grep yang harus lolos sebelum ruleset menyentuh server,
  • verifikasi-keamanan.sh — membandingkan ruleset dengan kebijakan-port.txt, menagih service merespons, lalu menjatuhkan vonis PASS atau FAIL beserta exit code-nya.

Tampilan Program Akhir #

Sesi berikut menunjukkan fase ketiga pada data contoh: server latihan python3 di port 8765 berperan sebagai service yang harus menjawab, lalu test suite membuktikan seluruh klaim proyek secara otomatis:

console
$ bash verifikasi-keamanan.sh latihan.nft kebijakan-port.txt http://127.0.0.1:8765/
== VERIFIKASI KEAMANAN SERVER ==
ruleset    : latihan.nft
kebijakan  : kebijakan-port.txt (port: 22 80 443)

[1] struktur ruleset
    OK    lolos 10 pemeriksaan validasi-ruleset.sh
[2] port yang dibuka ruleset
    ruleset  : 22 80 443
    kebijakan: 22 80 443
    OK    port ruleset sama dengan kebijakan
[3] service merespons
    OK    http://127.0.0.1:8765/ menjawab (kode 200)

HASIL: PASS — 3/3 pemeriksaan lulus
$ bash tests/run-tests.sh | tail -n 5
OK    service mati dilaporkan
OK    port 8765 kembali bebas setelah server mati
OK    argumen kurang keluar 4
OK    berkas hilang keluar 4
PASSED: 27/27 test lulus

Tiga baris OK pada laporan pertama adalah inti proyek: struktur ruleset diperiksa sebelum dipasang, port yang dibuka sama persis dengan kebijakan tertulis, dan service di balik port itu benar-benar menjawab. Ketika salah satu syarat runtuh — service mati, atau kebijakan berubah tanpa diikuti ruleset — laporan berganti FAIL dengan exit code yang bisa dibaca cron.

Tujuan Pembelajaran #

Setelah menyelesaikan proyek ini, kita mampu:

  • memetakan permukaan serangan server: service berjalan, port terbuka, alamat yang didengarkan, akun dengan shell, sampai paket yang tertinggal,
  • membedakan port yang hanya mendengarkan loopback dari yang terjangkau dari luar,
  • menulis ruleset nftables default-deny lengkap dan membacanya ulang baris per baris,
  • memvalidasi struktur ruleset dengan grep sebelum aturan diterapkan ke server,
  • menyusun kebijakan port sebagai berkas yang sama-sama dibaca manusia dan skrip,
  • menguji aturan dari sisi klien: port terbuka harus merespons, port lain harus ditolak,
  • menjadikan verifikasi sebagai kebiasaan berkala lewat cron dan checklist hardening berprioritas.

Prasyarat #

Proyek ini merangkai materi dari beberapa section sekaligus. Chapter yang paling sering dirujuk:

  • Firewall — chain, table, policy, dan stateful inspection sebagai fondasi seluruh ruleset,
  • nftables — sintaks nft, struktur table/chain/rule, dan ruleset default-deny,
  • ufw dan firewalld — padanan perintah frontend bila server Anda dikelola lewat ufw,
  • ss dan netstat — membaca socket yang mendengarkan beserta proses pemiliknya.

Tahapan Pengerjaan #

Proyek dikerjakan dalam tiga bagian berurutan. Setiap bagian diakhiri dengan artefak yang bisa diperiksa:

  1. Bagian 1: Audit Keamanan — memetakan service, port, user, kunci SSH, dan paket usang ke dalam satu laporan baseline.
  2. Bagian 2: Konfigurasi Firewall — menulis ruleset default-deny lengkap, membedahnya baris per baris, dan menolaknya bila strukturnya melenceng.
  3. Bagian 3: Verifikasi Keamanan — menguji port terbuka dan port tertutup dari sisi klien, lalu merangkum semuanya menjadi laporan PASS/FAIL yang dijadwalkan.

Mulailah dari Bagian 1 karena daftar izin di ruleset Bagian 2 dan seluruh pengujian Bagian 3 diambil langsung dari temuan audit — mengunci pintu sebelum tahu pintunya apa adanya hanya memindahkan masalah. Berkas sumber proyek (latihan.nft, kebijakan-port.txt, checklist-hardening.txt, tiga skrip di config/, serta tests/run-tests.sh dan docs/README.txt) tersedia pada bagian Berkas Sumber di bawah halaman setiap bagian, sehingga hasil pekerjaan bisa dibandingkan dengan versi referensi kapan saja.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

config/audit-baseline.sh

Unduh berkas
bash
#!/usr/bin/env bash
# audit-baseline.sh — kumpulkan baseline keamanan server ke satu laporan.
#
# Pemakaian:
#   audit-baseline.sh <dir-data> > laporan-audit.txt
#
# <dir-data> adalah miniatur filesystem server yang berisi snapshot:
#   services.txt      systemctl list-units --type=service
#                     --state=running --no-pager --no-legend
#   port-terbuka.txt  ss -tlnH
#   passwd            cuplikan /etc/passwd
#   upgradable.txt    apt list --upgradable
#   home/<user>/...   home directory tiap user (untuk authorized_keys)
#
# Kode keluar:
#   0  laporan berhasil dihasilkan (temuan dibaca isinya, bukan kodenya)
#   1  pemakaian salah: argumen hilang atau snapshot tidak lengkap

set -u

if [ "$#" -ne 1 ]; then
    printf 'pemakaian: audit-baseline.sh <dir-data>\n' >&2
    exit 1
fi
DATA=$1
if [ ! -d "$DATA" ]; then
    printf 'audit-baseline.sh: folder data tidak ditemukan: %s\n' "$DATA" >&2
    exit 1
fi
for berkas in services.txt port-terbuka.txt passwd upgradable.txt; do
    if [ ! -f "$DATA/$berkas" ]; then
        printf 'audit-baseline.sh: berkas snapshot hilang: %s/%s\n' \
            "$DATA" "$berkas" >&2
        exit 1
    fi
done

printf '===== AUDIT BASELINE KEAMANAN =====\n'
printf 'sumber data: %s\n\n' "$DATA"

# --- [1] service yang sedang berjalan ---------------------------------
jumlah_service=$(awk '$4 == "running" { c++ } END { print c + 0 }' \
    "$DATA/services.txt")
printf '[1] SERVICE BERJALAN: %d\n' "$jumlah_service"
awk '$4 == "running" { print $1 }' "$DATA/services.txt" | sort |
    sed 's/^/    /'
printf '\n'

# --- [2] port TCP yang terbuka ----------------------------------------
jumlah_port=$(grep -c . "$DATA/port-terbuka.txt")
printf '[2] PORT TCP TERBUKA: %d\n' "$jumlah_port"
awk '{
    ket = "semua interface"
    if (substr($4, 1, 4) == "127.")
        ket = "loopback saja"
    printf "    %-15s %s\n", $4, ket
}' "$DATA/port-terbuka.txt"
printf '\n'

# --- [3] user yang punya shell login ----------------------------------
jumlah_user=$(awk -F: '$7 !~ /(nologin|false)$/ { c++ } END { print c + 0 }' \
    "$DATA/passwd")
printf '[3] USER DENGAN SHELL LOGIN: %d\n' "$jumlah_user"
awk -F: '$7 !~ /(nologin|false)$/ { printf "    %-8s %s\n", $1, $7 }' \
    "$DATA/passwd"
printf '\n'

# --- [4] kunci SSH per user --------------------------------------------
printf '[4] KUNCI SSH PER USER:\n'
awk -F: '$7 !~ /(nologin|false)$/ { print $1, $6 }' "$DATA/passwd" |
while read -r user home; do
    kunci="$DATA$home/.ssh/authorized_keys"
    if [ -f "$kunci" ]; then
        printf '    %-8s %d kunci\n' "$user" "$(grep -c . "$kunci")"
    else
        printf '    %-8s tanpa authorized_keys\n' "$user"
    fi
done
printf '\n'

# --- [5] paket yang tertinggal pembaruan --------------------------------
jumlah_paket=$(grep -c 'upgradable from' "$DATA/upgradable.txt")
printf '[5] PAKET SIAP DIPERBARUI: %d\n' "$jumlah_paket"
grep 'upgradable from' "$DATA/upgradable.txt" | awk '{ print "    " $1 }'
printf '\n'

# --- ringkasan temuan ----------------------------------------------------
jumlah_luar=$(awk '{ if (substr($4, 1, 4) != "127.") c++ }
    END { print c + 0 }' "$DATA/port-terbuka.txt")
tanpa_kunci=$(awk -F: '$7 !~ /(nologin|false)$/ { print $1, $6 }' \
    "$DATA/passwd" |
while read -r user home; do
    [ -f "$DATA$home/.ssh/authorized_keys" ] || printf '%s\n' "$user"
done | paste -sd' ' -)
jumlah_tanpa=$(printf '%s' "$tanpa_kunci" | wc -w)
printf 'RINGKASAN TEMUAN:\n'
printf '    - %d port mendengarkan di semua interface (terjangkau dari luar)\n' \
    "$jumlah_luar"
printf '    - %d akun shell tanpa authorized_keys: %s\n' \
    "$jumlah_tanpa" "$tanpa_kunci"
printf '    - %d paket tertinggal pembaruan\n' "$jumlah_paket"
exit 0

config/validasi-ruleset.sh

Unduh berkas
bash
#!/usr/bin/env bash
# validasi-ruleset.sh — periksa struktur ruleset nftables sebelum diterapkan.
#
# Pemakaian:
#   validasi-ruleset.sh <ruleset.nft>
#
# Pemeriksaan memakai grep pada bentuk baku yang kita sepakati di
# proyek ini; ubah daftar port = ubah ruleset, kebijakan-port.txt,
# dan pola dport di bawah ini bersama-sama.
#
# Kode keluar:
#   0  seluruh pemeriksaan lolos
#   N  jumlah pemeriksaan yang gagal
#   1  berkas ruleset tidak ditemukan

set -u

berkas=${1:-}
if [ -z "$berkas" ] || [ ! -f "$berkas" ]; then
    printf 'validasi-ruleset.sh: berkas ruleset tidak ditemukan: %s\n' \
        "$berkas" >&2
    exit 1
fi

gagal=0

# periksa <pola> <pesan>: pola harus ditemukan.
periksa() {
    if grep -qE -- "$1" "$berkas"; then
        printf 'OK    %s\n' "$2"
    else
        printf 'GAGAL %s\n' "$2"
        gagal=$((gagal + 1))
    fi
}

# tolak <pola> <pesan>: pola justru tidak boleh ada.
tolak() {
    if grep -qE -- "$1" "$berkas"; then
        printf 'GAGAL %s\n' "$2"
        gagal=$((gagal + 1))
    else
        printf 'OK    %s\n' "$2"
    fi
}

periksa '^#!/usr/sbin/nft -f' 'shebang nft -f (bisa dieksekusi langsung)'
periksa '^flush ruleset' 'ruleset lama dibuang dulu (flush ruleset)'
periksa '^table inet filter' 'table inet filter dideklarasikan'
periksa 'policy drop;' 'kebijakan chain input: drop'
periksa 'iifname "lo" accept' 'loopback diizinkan'
periksa 'ct state established,related accept' 'balasan koneksi sah diizinkan'
periksa 'tcp dport \{ 22, 80, 443 \} accept' 'hanya SSH, HTTP, HTTPS yang dibuka'
periksa 'limit rate .* log prefix' 'paket yang jatuh ke policy dicatat dengan rem'
tolak 'policy accept;' 'tidak ada kebijakan accept yang menyelip'
tolak '^[[:space:]]*accept[[:space:]]*$' 'tidak ada aturan accept tanpa syarat'

exit "$gagal"

config/verifikasi-keamanan.sh

Unduh berkas
bash
#!/usr/bin/env bash
# verifikasi-keamanan.sh — buktikan ruleset, kebijakan port, dan service
# hidup dalam satu laporan PASS/FAIL.
#
# Pemakaian:
#   verifikasi-keamanan.sh <ruleset.nft> <kebijakan-port.txt> <url-uji>
#
# Pemeriksaan:
#   [1] struktur ruleset  — memanggil validasi-ruleset.sh dari folder sama
#   [2] port ruleset == daftar port di kebijakan-port.txt
#   [3] service merespons — curl ke <url-uji>; exit 0 = ada yang menjawab
#
# Kode keluar:
#   0  semua pemeriksaan lulus (PASS)
#   N  jumlah pemeriksaan yang gagal (1-3)
#   4  pemakaian salah: argumen kurang atau berkas tidak ada

set -u
DIR=$(cd "$(dirname "$0")" && pwd)

if [ "$#" -ne 3 ]; then
    printf 'pemakaian: verifikasi-keamanan.sh <ruleset> <kebijakan-port> <url-uji>\n' >&2
    exit 4
fi
RULESET=$1
KEBIJAKAN=$2
URL=$3

for berkas in "$RULESET" "$KEBIJAKAN"; do
    if [ ! -f "$berkas" ]; then
        printf 'verifikasi-keamanan.sh: berkas tidak ditemukan: %s\n' \
            "$berkas" >&2
        exit 4
    fi
done
if [ ! -f "$DIR/validasi-ruleset.sh" ]; then
    printf 'verifikasi-keamanan.sh: validasi-ruleset.sh tidak ada di %s\n' \
        "$DIR" >&2
    exit 4
fi

gagal=0

# Daftar port di kebijakan dihitung lebih dulu untuk kepala laporan.
port_kebijakan=$(awk '$1 ~ /^[0-9]+$/ { print $1 }' "$KEBIJAKAN" |
    sort -n | paste -sd' ' -)

printf '== VERIFIKASI KEAMANAN SERVER ==\n'
printf 'ruleset    : %s\n' "$RULESET"
printf 'kebijakan  : %s (port: %s)\n\n' "$KEBIJAKAN" \
    "${port_kebijakan:-(kosong)}"

# --- [1] struktur ruleset -----------------------------------------------
out=$(bash "$DIR/validasi-ruleset.sh" "$RULESET")
status_val=$?
jumlah_ok=$(printf '%s\n' "$out" | grep -c '^OK')

printf '[1] struktur ruleset\n'
if [ "$status_val" -eq 0 ]; then
    printf '    OK    lolos %d pemeriksaan validasi-ruleset.sh\n' "$jumlah_ok"
else
    printf '    GAGAL ditolak validasi-ruleset.sh (%d pemeriksaan gagal)\n' \
        "$status_val"
    gagal=$((gagal + 1))
fi

# --- [2] port yang dibuka ruleset == kebijakan ---------------------------
port_ruleset=$(grep -oE 'dport[[:space:]]+\{[^}]*\}' "$RULESET" |
    grep -oE '[0-9]+' | sort -n | paste -sd' ' -)
printf '[2] port yang dibuka ruleset\n'
printf '    ruleset  : %s\n' "${port_ruleset:-(kosong)}"
printf '    kebijakan: %s\n' "${port_kebijakan:-(kosong)}"
if [ -n "$port_kebijakan" ] && [ "$port_ruleset" = "$port_kebijakan" ]; then
    printf '    OK    port ruleset sama dengan kebijakan\n'
else
    printf '    GAGAL port ruleset berbeda dengan kebijakan\n'
    gagal=$((gagal + 1))
fi

# --- [3] service merespons ------------------------------------------------
printf '[3] service merespons\n'
kode=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$URL")
curl_exit=$?
if [ "$curl_exit" -eq 0 ]; then
    printf '    OK    %s menjawab (kode %s)\n' "$URL" "$kode"
else
    printf '    GAGAL %s tidak menjawab (curl exit %d)\n' "$URL" "$curl_exit"
    gagal=$((gagal + 1))
fi

# --- ringkasan ------------------------------------------------------------
lulus=$((3 - gagal))
printf '\nHASIL: '
if [ "$gagal" -eq 0 ]; then
    printf 'PASS — 3/3 pemeriksaan lulus\n'
else
    printf 'FAIL — %d/3 pemeriksaan lulus\n' "$lulus"
fi
exit "$gagal"

tests/run-tests.sh

Unduh berkas
bash
#!/usr/bin/env bash
# run-tests.sh — test suite untuk proyek firewall dan hardening server.
# Suite menyiapkan folder kerja terisolasi (mktemp), menyalin berkas
# config/, membangun fixture audit, menyalakan server latihan python3
# sebagai pengganti service sungguhan, lalu menguji: laporan audit dari
# fixture, validasi ruleset (valid dan rusak), verifikasi keamanan
# (PASS dan FAIL), beserta exit code tiap cabangnya.
# Jalankan dari mana saja: bash tests/run-tests.sh

set -u

SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJ_DIR=$(dirname "$SCRIPT_DIR")
CONFIG="$PROJ_DIR/config"

PASS=0
FAIL=0
WORK=""
SERVER=""

# assert_eq <harapan> <nyata> <pesan>: bandingkan dua nilai.
assert_eq() {
    if [ "$1" = "$2" ]; then
        PASS=$((PASS + 1))
        printf 'OK    %s\n' "$3"
    else
        FAIL=$((FAIL + 1))
        printf 'GAGAL %s: harapan %s, nyata %s\n' "$3" "$1" "$2"
    fi
}

# assert_ada <pola> <berkas> <pesan>: pola harus ditemukan di berkas.
assert_ada() {
    if grep -qF -- "$1" "$2"; then
        PASS=$((PASS + 1))
        printf 'OK    %s\n' "$3"
    else
        FAIL=$((FAIL + 1))
        printf 'GAGAL %s: pola tidak ditemukan di %s\n' "$3" "$2"
    fi
}

bersihkan() {
    if [ -n "$SERVER" ]; then
        kill "$SERVER" 2> /dev/null
    fi
    if [ -n "$WORK" ]; then
        rm -rf "$WORK"
    fi
}
trap bersihkan EXIT

# --- siapkan folder kerja terisolasi ------------------------------------
WORK=$(mktemp -d)
cp "$CONFIG/latihan.nft" "$CONFIG/kebijakan-port.txt" \
    "$CONFIG/audit-baseline.sh" "$CONFIG/validasi-ruleset.sh" \
    "$CONFIG/verifikasi-keamanan.sh" "$WORK/"
cd "$WORK" || exit 1

# --- uji 1: audit menghasilkan laporan deterministik dari fixture --------
mkdir -p audit-data/home/budi/.ssh audit-data/home/siti/.ssh \
    audit-data/home/deploy audit-data/root
cat > audit-data/services.txt <<'EOF'
nginx.service          loaded active running A high performance web server and reverse proxy
postgresql.service      loaded active running PostgreSQL RDBMS
redis-server.service    loaded active running Advanced key-value store
ssh.service             loaded active running OpenBSD Secure Shell server
EOF
cat > audit-data/port-terbuka.txt <<'EOF'
LISTEN 0      128        0.0.0.0:22           0.0.0.0:*
LISTEN 0      511        0.0.0.0:80           0.0.0.0:*
LISTEN 0      511        0.0.0.0:443          0.0.0.0:*
LISTEN 0      511        0.0.0.0:8080         0.0.0.0:*
LISTEN 0      24         127.0.0.1:5432       0.0.0.0:*
LISTEN 0      511        127.0.0.1:6379       0.0.0.0:*
EOF
cat > audit-data/passwd <<'EOF'
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
budi:x:1000:1000:Budi Santoso:/home/budi:/bin/bash
siti:x:1001:1001:Siti Rahma:/home/siti:/bin/bash
deploy:x:1002:1002:Deploy Bot:/home/deploy:/bin/bash
EOF
printf 'ssh-ed25519 AAAAC3NzaC1lZDI1...kunci-budi budi@laptop-budi\n' \
    > audit-data/home/budi/.ssh/authorized_keys
printf 'ssh-ed25519 AAAAC3NzaC1lZDI1...kunci-siti-1 siti@laptop-siti\nssh-ed25519 AAAAC3NzaC1lZDI1...kunci-siti-2 siti@telepon-siti\n' \
    > audit-data/home/siti/.ssh/authorized_keys
cat > audit-data/upgradable.txt <<'EOF'
Listing... Done
libssl3/stable 3.0.15-1~deb12u1 [upgradable from: 3.0.16-1~deb12u1]
openssl/stable 3.0.15-1~deb12u1 [upgradable from: 3.0.16-1~deb12u1]
redis-server/stable 5:7.0.15-1~deb12u1 [upgradable from: 5:7.0.16-1~deb12u1]
EOF

bash audit-baseline.sh audit-data > laporan-audit.txt
assert_eq 0 "$?" "audit menghasilkan laporan (exit 0)"
assert_ada '[1] SERVICE BERJALAN: 4' laporan-audit.txt \
    "laporan memuat 4 service berjalan"
assert_ada '[2] PORT TCP TERBUKA: 6' laporan-audit.txt \
    "laporan memuat 6 port terbuka"
assert_ada '0.0.0.0:8080    semua interface' laporan-audit.txt \
    "port 8080 tercatat terbuka ke luar"
assert_ada '2 akun shell tanpa authorized_keys' laporan-audit.txt \
    "temuan akun shell tanpa kunci"
assert_ada '3 paket tertinggal pembaruan' laporan-audit.txt \
    "temuan paket tertinggal"
bash audit-baseline.sh > /dev/null 2>&1
assert_eq 1 "$?" "audit tanpa argumen keluar 1"

# --- uji 2: ruleset valid lolos validator --------------------------------
bash validasi-ruleset.sh latihan.nft > hasil-validasi.txt
assert_eq 0 "$?" "ruleset latihan.nft lolos validasi"
jumlah=$(grep -c '^OK' hasil-validasi.txt)
assert_eq 10 "$jumlah" "sepuluh pemeriksaan validasi semuanya OK"

# --- uji 3: ruleset rusak ditolak validator -------------------------------
cat > rusak.nft <<'EOF'
# rusak.nft — ruleset longgar: semua koneksi diterima.
table inet filter {
    chain input {
        type filter hook input priority filter; policy accept;
        accept
    }
}
EOF
bash validasi-ruleset.sh rusak.nft > hasil-rusak.txt
assert_eq 9 "$?" "ruleset rusak ditolak (9 pemeriksaan gagal)"
assert_ada 'GAGAL kebijakan chain input: drop' hasil-rusak.txt \
    "policy yang salah dilaporkan"
assert_ada 'GAGAL tidak ada aturan accept tanpa syarat' hasil-rusak.txt \
    "accept tanpa syarat dilaporkan"
bash validasi-ruleset.sh tidak-ada.nft > /dev/null 2>&1
assert_eq 1 "$?" "berkas ruleset hilang keluar 1"

# --- uji 4: verifikasi PASS pada port sesuai kebijakan --------------------
printf 'server latihan firewall\n' > index.html
python3 -m http.server 8765 --bind 127.0.0.1 > /dev/null 2>&1 &
SERVER=$!
siap=0
for i in 1 2 3 4 5 6 7 8 9 10; do
    if curl -fsS -o /dev/null http://127.0.0.1:8765/ 2> /dev/null; then
        siap=1
        break
    fi
    sleep 0.2
done
assert_eq 1 "$siap" "server latihan siap di port 8765"

bash verifikasi-keamanan.sh latihan.nft kebijakan-port.txt \
    http://127.0.0.1:8765/ > hasil-verifikasi.txt
assert_eq 0 "$?" "verifikasi PASS keluar 0"
assert_ada 'HASIL: PASS' hasil-verifikasi.txt "laporan berakhir PASS"
assert_ada 'port ruleset sama dengan kebijakan' hasil-verifikasi.txt \
    "port ditandai sesuai kebijakan"
assert_ada 'menjawab (kode 200)' hasil-verifikasi.txt \
    "service ditandai merespons"

# --- uji 5: kebijakan berbeda -> port melenceng ----------------------------
grep -v '^443' kebijakan-port.txt > kebijakan-tanpa-443.txt
bash verifikasi-keamanan.sh latihan.nft kebijakan-tanpa-443.txt \
    http://127.0.0.1:8765/ > hasil-beda.txt
assert_eq 1 "$?" "kebijakan beda keluar 1"
assert_ada 'port ruleset berbeda dengan kebijakan' hasil-beda.txt \
    "perbedaan port dilaporkan"

# --- uji 6: ruleset rusak di tangan verifikasi -----------------------------
bash verifikasi-keamanan.sh rusak.nft kebijakan-port.txt \
    http://127.0.0.1:8765/ > hasil-verif-rusak.txt
assert_eq 2 "$?" "ruleset rusak + port melenceng keluar 2"
assert_ada 'ditolak validasi-ruleset.sh' hasil-verif-rusak.txt \
    "kegagalan validator dilaporkan"

# --- uji 7: service mati -> FAIL, port kembali bebas -------------------------
kill "$SERVER" 2> /dev/null
wait "$SERVER" 2> /dev/null
SERVER=""
bash verifikasi-keamanan.sh latihan.nft kebijakan-port.txt \
    http://127.0.0.1:8765/ > hasil-mati.txt
assert_eq 1 "$?" "service mati keluar 1"
assert_ada 'tidak menjawab' hasil-mati.txt "service mati dilaporkan"
jumlah=$(ss -tlnH '( sport = :8765 )' | grep -c .)
assert_eq 0 "$jumlah" "port 8765 kembali bebas setelah server mati"

# --- uji 8: pemakaian salah --------------------------------------------------
bash verifikasi-keamanan.sh latihan.nft > /dev/null 2>&1
assert_eq 4 "$?" "argumen kurang keluar 4"
bash verifikasi-keamanan.sh tidak-ada.nft kebijakan-port.txt \
    http://127.0.0.1:8765/ > /dev/null 2>&1
assert_eq 4 "$?" "berkas hilang keluar 4"

# --- ringkasan ----------------------------------------------------------------
if [ "$FAIL" -eq 0 ]; then
    printf 'PASSED: %d/%d test lulus\n' "$PASS" "$PASS"
else
    printf 'FAILED: %d/%d test lulus\n' "$PASS" "$((PASS + FAIL))"
fi
[ "$FAIL" -eq 0 ]