Seluruh bab sebelumnya mengaudit kode yang kita tulis. Namun aplikasi Java modern rata-rata berdiri di atas ratusan library pihak ketiga — framework, driver, utilitas — dan setiap barisnya sama-sama berpotensi berkerentanan, tanpa satu pun baris kode kita yang salah. Dunia menyebut tiap cacat itu dengan nomor bernama CVE (Common Vulnerabilities and Exposures): identitas publik yang menjelaskan artefak mana, rentang versi mana, dan dampak apa. Log4Shell (CVE-2021-44228) pada akhir 2021 memperlihatkan skala masalahnya: satu celah di library logging yang dipakai jutaan aplikasi cukup untuk memicu gelombang perbaikan serentak di seluruh industri. Kabar baiknya, manajemen kerentanan dependency adalah proses yang bisa diotomasi penuh — pindai secara terjadwal, perbaiki secara terarah. Bab ini membangun logika pemindainya dari nol, lalu memasangnya ke pipeline build nyata.

Logika Pemindai: Artefak Versi dan Rentang #

Inti setiap pemindai dependensi sama: cocokkan daftar dependensi proyek terhadap database kerentanan. Satu entri CVE memuat nama artefak, rentang versi terdampak, dan versi yang memuat perbaikan — pembandingan versi menjadi sembunyian yang menentukan. Program berikut membangun versi mini dari pemindai itu: basis pengetahuan dua CVE nyata, empat dependensi contoh, dan pembanding versi numerik bertitik:

import java.util.ArrayList;
import java.util.List;

public class PindaiDependensi {

    // Satu dependensi proyek: nama artefak dan versi terpasang.
    record Dependensi(String artefak, String versi) { }

    // Satu entri kerentanan: rentang versi terdampak (mulai sampai sebelum
    // batas), versi perbaikan, dan deskripsi singkatnya.
    record Cve(String id, String artefak, String mulai, String batas,
               String perbaikan, String deskripsi) { }

    // Basis pengetahuan mini dua entri - versi sederhana dari database CVE.
    private static final List<Cve> BASIS_CVE = List.of(
            new Cve("CVE-2021-44228", "log4j-core", "2.0", "2.16.0", "2.17.1",
                    "Log4Shell: JNDI lookup pada pesan log memicu unduh dan eksekusi class"),
            new Cve("CVE-2015-7501", "commons-collections", "3.0", "3.2.2", "3.2.2",
                    "InvokerTransformer pada deserialisasi memicu eksekusi perintah"));

    public static void main(String[] args) {
        List<Dependensi> proyek = List.of(
                new Dependensi("log4j-core", "2.14.1"),
                new Dependensi("commons-collections", "3.2.1"),
                new Dependensi("jackson-databind", "2.15.0"),
                new Dependensi("log4j-core", "2.17.1"));

        System.out.println("=== Pemindaian " + proyek.size() + " dependensi "
                + "(basis " + BASIS_CVE.size() + " CVE) ===");
        int temuan = 0;
        for (Dependensi d : proyek) {
            List<Cve> kena = cariTemuan(d);
            if (kena.isEmpty()) {
                System.out.println(d.artefak() + " " + d.versi()
                        + " -> aman (tidak ada temuan)");
            } else {
                for (Cve c : kena) {
                    temuan++;
                    System.out.println(d.artefak() + " " + d.versi()
                            + " -> RENTAN " + c.id() + " (perbaikan: " + c.perbaikan() + ")");
                    System.out.println("    " + c.deskripsi());
                }
            }
        }
        System.out.println("Temuan: " + temuan + " kerentanan");
    }

    static List<Cve> cariTemuan(Dependensi d) {
        List<Cve> kena = new ArrayList<>();
        for (Cve c : BASIS_CVE) {
            if (!c.artefak().equals(d.artefak())) {
                continue;
            }
            boolean diAtasAtauSama = bandingkanVersi(d.versi(), c.mulai()) >= 0;
            boolean diBawahBatas = bandingkanVersi(d.versi(), c.batas()) < 0;
            if (diAtasAtauSama && diBawahBatas) {
                kena.add(c);
            }
        }
        return kena;
    }

    // Bandingkan versi numerik bertitik: 2.14.1 < 2.16.0 < 2.17.1.
    static int bandingkanVersi(String a, String b) {
        String[] bagianA = a.split("\\.");
        String[] bagianB = b.split("\\.");
        int panjang = Math.max(bagianA.length, bagianB.length);
        for (int i = 0; i < panjang; i++) {
            int na = i < bagianA.length ? Integer.parseInt(bagianA[i]) : 0;
            int nb = i < bagianB.length ? Integer.parseInt(bagianB[i]) : 0;
            if (na != nb) {
                return Integer.compare(na, nb);
            }
        }
        return 0;
    }
}

Output yang Dihasilkan:

text
=== Pemindaian 4 dependensi (basis 2 CVE) ===
log4j-core 2.14.1 -> RENTAN CVE-2021-44228 (perbaikan: 2.17.1)
    Log4Shell: JNDI lookup pada pesan log memicu unduh dan eksekusi class
commons-collections 3.2.1 -> RENTAN CVE-2015-7501 (perbaikan: 3.2.2)
    InvokerTransformer pada deserialisasi memicu eksekusi perintah
jackson-databind 2.15.0 -> aman (tidak ada temuan)
log4j-core 2.17.1 -> aman (tidak ada temuan)
Temuan: 2 kerentanan

Pemindaian menemukan dua kerentanan: log4j-core 2.14.1 jatuh di rentang CVE-2021-44228 (2.0 hingga sebelum 2.16.0), dan commons-collections 3.2.1 jatuh di rentang CVE-2015-7501 (3.0 hingga sebelum 3.2.2). Dua dependensi lainnya bersih. Inilah persis pekerjaan alat produksi — hanya dengan database berpuluh ribu entri yang diperbarui setiap hari.

Bedah baris-per-baris untuk dua method intinya:

  1. Baris 56-58 (if (!c.artefak().equals(d.artefak()))): pencocokan pertama selalu nama artefak — CVE hanya relevan untuk pustaka yang benar-benar terpasang. Pemindai sungguhan juga memeriksa coordinate lengkap (group + artifact) karena nama artefak yang sama bisa dimiliki dua grup berbeda.
  2. Baris 60-61 (diAtasAtauSama && diBawahBatas): keanggotaan rentang dinyatakan sebagai [mulai, batas) — versi terdampak berada pada atau setelah mulai dan sebelum batas. Konvensi setengah-terbuka ini menghindari keraguan pada tepi rentang: batas selalu versi pertama yang dianggap bersih.
  3. Baris 69-80 (bandingkanVersi): versi dibandingkan per komponen numerik dari kiri, komponen hilang dihitung nol. Bandingkan digit demi digit bukan lewat String.compareTo() — string "2.10.0" dianggap lebih besar dari "2.9.0" padahal secara semantik sebaliknya.
  4. Baris 70-71 (split("\\.")): titik di-escape karena pada regex titik berarti “karakter apa pun”. Versi dunia nyata berisi sufiks (2.3.1, 2.3.1-r1, RC1) yang menuntut parser lebih kaya — Maven dan Gradle menyediakannya bawaan.

Memasang Pemindaian ke Build #

Pemindai produksi tidak berupa class buatan sendiri, melainkan plugin yang menghubungkan grafik dependensi build dengan database NVD (National Vulnerability Database). OWASP Dependency-Check adalah pilihan open source yang berjalan pada Maven dan Gradle:

xml Konfigurasi dependency-check-maven pada pom.xml
<plugin>
    <groupId>org.owasp</groupId>
    <artifactId>dependency-check-maven</artifactId>
    <version>13.0.0</version>
    <configuration>
        <failBuildOnCVSS>7</failBuildOnCVSS>  <!-- gagal build bila temuan CVSS >= 7 -->
        <nvdApiKey>${env.NVD_API_KEY}</nvdApiKey> <!-- kunci dari env, bukan hardcode -->
    </configuration>
</plugin>
kotlin Audit dependency pada Gradle
// Jalankan: gradle dependencyCheckAnalyze
dependencies {
    implementation("org.apache.logging.log4j:log4j-core:2.17.1") // kunci versi eksplisit
}
// Gradle juga menyediakan analisis konflik versi bawaan:
// gradle dependencies --configuration runtimeClasspath

Layanan seperti Snyk, GitHub Dependabot, dan Sonatype menambahkan dimensi pemantauan berkelanjutan: pemindaian otomatis pada setiap commit, notifikasi ketika CVE baru mengenai versi yang kita pakai, dan usulan pull request peningkatan versi. Alat berganda ini bukan pemborosan — database NVD yang menjadi sumber bersama mereka diperbarui setiap hari, dan pipeline yang hanya dipindai sekali per rilis akan ketinggalan CVE yang dipublikasikan sehari setelah pemindaian.

Kunci Versi SBOM dan Disiplin Rantai Pasok #

Dua praktik penutup menjaga hasil pemindaian tetap bermakna. Pertama, kunci versi eksplisit — deklarasi dependensi menyebut versi pasti (atau rentang terkendali), bukan LATEST atau SNAPSHOT yang bergerak sendiri; build yang tidak bisa direproduksi tidak bisa diaudit, karena dependensi yang dipindai hari ini bukan yang berjalan besok. Kedua, SBOM (software bill of materials): berkas daftar seluruh komponen beserta versinya — format standar seperti CycloneDX atau SPDX — yang dilampirkan pada setiap rilis. Ketika CVE baru diumumkan, tim operasi tidak memindai ulang seluruh artefak; cukup mencari SBOM mana yang memuat artefak terdampak, dan dalam hitungan menit diketahui aplikasi mana yang harus diperbarui.

Kerentanan dependency adalah lapisan terakhir yang kita audit. Satu bab tersisa untuk merangkai seluruh lapisan — input, encoding, autentikasi, kriptografi, dependency, hingga pemantauan — menjadi satu bangunan keamanan aplikasi Java yang utuh.

Referensi #

Berkas Sumber

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

PindaiDependensi.java

Unduh berkas
java
import java.util.ArrayList;
import java.util.List;

public class PindaiDependensi {

    // Satu dependensi proyek: nama artefak dan versi terpasang.
    record Dependensi(String artefak, String versi) { }

    // Satu entri kerentanan: rentang versi terdampak (mulai sampai sebelum
    // batas), versi perbaikan, dan deskripsi singkatnya.
    record Cve(String id, String artefak, String mulai, String batas,
               String perbaikan, String deskripsi) { }

    // Basis pengetahuan mini dua entri - versi sederhana dari database CVE.
    private static final List<Cve> BASIS_CVE = List.of(
            new Cve("CVE-2021-44228", "log4j-core", "2.0", "2.16.0", "2.17.1",
                    "Log4Shell: JNDI lookup pada pesan log memicu unduh dan eksekusi class"),
            new Cve("CVE-2015-7501", "commons-collections", "3.0", "3.2.2", "3.2.2",
                    "InvokerTransformer pada deserialisasi memicu eksekusi perintah"));

    public static void main(String[] args) {
        List<Dependensi> proyek = List.of(
                new Dependensi("log4j-core", "2.14.1"),
                new Dependensi("commons-collections", "3.2.1"),
                new Dependensi("jackson-databind", "2.15.0"),
                new Dependensi("log4j-core", "2.17.1"));

        System.out.println("=== Pemindaian " + proyek.size() + " dependensi "
                + "(basis " + BASIS_CVE.size() + " CVE) ===");
        int temuan = 0;
        for (Dependensi d : proyek) {
            List<Cve> kena = cariTemuan(d);
            if (kena.isEmpty()) {
                System.out.println(d.artefak() + " " + d.versi()
                        + " -> aman (tidak ada temuan)");
            } else {
                for (Cve c : kena) {
                    temuan++;
                    System.out.println(d.artefak() + " " + d.versi()
                            + " -> RENTAN " + c.id() + " (perbaikan: " + c.perbaikan() + ")");
                    System.out.println("    " + c.deskripsi());
                }
            }
        }
        System.out.println("Temuan: " + temuan + " kerentanan");
    }

    static List<Cve> cariTemuan(Dependensi d) {
        List<Cve> kena = new ArrayList<>();
        for (Cve c : BASIS_CVE) {
            if (!c.artefak().equals(d.artefak())) {
                continue;
            }
            boolean diAtasAtauSama = bandingkanVersi(d.versi(), c.mulai()) >= 0;
            boolean diBawahBatas = bandingkanVersi(d.versi(), c.batas()) < 0;
            if (diAtasAtauSama && diBawahBatas) {
                kena.add(c);
            }
        }
        return kena;
    }

    // Bandingkan versi numerik bertitik: 2.14.1 < 2.16.0 < 2.17.1.
    static int bandingkanVersi(String a, String b) {
        String[] bagianA = a.split("\\.");
        String[] bagianB = b.split("\\.");
        int panjang = Math.max(bagianA.length, bagianB.length);
        for (int i = 0; i < panjang; i++) {
            int na = i < bagianA.length ? Integer.parseInt(bagianA[i]) : 0;
            int nb = i < bagianB.length ? Integer.parseInt(bagianB[i]) : 0;
            if (na != nb) {
                return Integer.compare(na, nb);
            }
        }
        return 0;
    }
}