SAST dan DAST: Menguji Keamanan Aplikasi dari Kode sampai Aplikasi yang Jalan
Bug yang lolos code review bisa ditangkap dengan dua cara: membaca kodenya (SAST) atau menyerang aplikasinya yang sedang jalan (DAST). Di sini dibahas bedanya, lalu praktik langsung dengan Semgrep, gitleaks, OWASP ZAP, dan Nuclei sampai masuk ke GitHub Actions.

Bayangin tugas besar kelompok kamu adalah toko buku online. Ada fitur pencarian, dan kodenya kira-kira begini:
const sql = "SELECT * FROM books WHERE title LIKE '%" + req.query.q + "%'";Teman satu kelompok sudah me-review, fiturnya dicoba dan jalan, semua senang. Lalu waktu demo, dosen penguji mengetik ' OR '1'='1 di kotak pencarian dan seluruh isi tabel keluar. Kalau tabel yang kena berisi data pengguna, ceritanya jadi jauh lebih serius.
Bug seperti ini lolos review karena reviewer biasanya mengecek apakah fiturnya benar, dan untuk input yang wajar fiturnya memang benar. Yang jarang dicek adalah apa yang terjadi kalau inputnya sengaja dibuat jahat. Ada dua cara yang umum dipakai untuk menangkapnya lebih awal: membaca kodenya dengan alat (SAST), atau menyerang aplikasinya yang sedang jalan (DAST). Di tulisan ini kita bahas keduanya, lengkap dengan perintah yang bisa kamu coba di laptop.
SAST dan DAST itu apa
SAST (Static Application Security Testing) menganalisis source code, bytecode, atau binary tanpa menjalankan aplikasinya. Alatnya bisa melihat isi kode, jadi pendekatan ini disebut white-box. Hasilnya menunjuk file dan nomor baris, misalnya src/routes/books.js:8.
DAST (Dynamic Application Security Testing) bekerja dari arah sebaliknya. Alat DAST nggak diberi kode sama sekali, cuma URL aplikasi yang sedang jalan. Dari situ alat itu mengirim request HTTP seperti pengguna biasa atau penyerang, lalu memeriksa respons yang kembali. Pendekatan ini disebut black-box.
![]()
SAST bisa jalan sejak kode masih di laptop atau di pull request, sedangkan DAST baru bisa jalan setelah aplikasi di-deploy, minimal ke staging. Makin ke kanan sebuah bug ditemukan, makin mahal memperbaikinya.
Istilah yang bakal sering muncul
Istilah berikut muncul di hampir semua laporan scanner:
- False positive adalah temuan yang ternyata bukan masalah. False negative kebalikannya, yaitu celah nyata yang nggak dilaporkan, dan ini lebih berbahaya karena memberi rasa aman palsu.
- Ruleset adalah kumpulan aturan yang dipakai scanner. Semgrep punya ruleset seperti
p/owasp-top-tenataup/javascript, dan ruleset yang kamu pilih menentukan apa saja yang bisa ketemu. - Severity adalah tingkat keparahan temuan. Semgrep memakai INFO, WARNING, dan ERROR (versi barunya juga mengenal LOW sampai CRITICAL), sedangkan ZAP memakai Low, Medium, dan High.
- CWE (Common Weakness Enumeration) adalah katalog jenis kelemahan, misalnya CWE-89 untuk SQL injection. CVE (Common Vulnerabilities and Exposures) adalah nomor untuk satu celah spesifik di produk tertentu, misalnya celah di library versi sekian.
- Baseline scan dan active scan adalah dua mode DAST. Baseline hanya mengamati respons tanpa menyerang, sedangkan active scan mengirim payload serangan sungguhan.
- Quality gate adalah syarat otomatis di pipeline CI, misalnya pull request nggak boleh di-merge selama masih ada temuan ERROR.
Cara kerja SAST
Alat SAST modern mengurai kode dulu menjadi pohon sintaks (AST, abstract syntax tree), lalu rule dicocokkan ke struktur pohon itu. Cara ini jauh lebih teliti daripada grep. eval(x) dan eval( x ) dianggap sama, dan komentar yang kebetulan berisi kata eval nggak ikut ditandai.
![]()
Banyak alat SAST juga melakukan taint analysis, yaitu melacak data dari sumber yang nggak dipercaya (misalnya req.query.q) sampai ke fungsi berbahaya seperti db.query atau eval. Kalau data itu sampai ke sana tanpa dibersihkan dulu, alat melaporkannya.
Kelemahan SAST ada di konteks. Alatnya nggak tahu apakah fungsi itu cuma bisa dipanggil admin, apakah ada validasi di middleware lain, atau apakah server dipasang di belakang WAF. Karena itu false positive di SAST relatif sering, dan setiap temuan tetap perlu dibaca manusia.
Cara kerja DAST
DAST bekerja dalam tiga tahap. Spider menjelajah aplikasi, mengikuti link dan form untuk mengumpulkan daftar URL. Passive scan memeriksa setiap respons yang lewat tanpa mengubah apa pun, misalnya header keamanan yang belum dipasang atau cookie tanpa flag HttpOnly. Active scan mengirim payload ke setiap parameter, seperti ' OR 1=1-- untuk SQL injection atau <script> untuk XSS, lalu melihat apakah respons aplikasi berubah dengan cara yang mencurigakan.
![]()
Kelebihan DAST, temuannya relatif bisa dipercaya karena terbukti lewat HTTP. Kalau payload XSS muncul utuh di respons, berarti memang ada masalah. DAST juga menangkap hal yang nggak kelihatan dari kode, misalnya konfigurasi server, header dari reverse proxy, atau library frontend lama yang ternyata ikut ter-deploy. Kelemahannya, DAST hanya bisa menguji halaman yang berhasil ditemukan spider, dan temuannya cuma menunjuk URL dan parameter. Kamu masih harus mencari sendiri baris kode penyebabnya.
Bedanya di mana
![]()
Gambar di atas merangkum sifat dasarnya. Tabel berikut lebih ke sisi praktis, yaitu kasus apa yang bisa ditangkap masing-masing:
| Kasus | SAST | DAST |
|---|---|---|
| SQL injection dari string yang disambung | Bisa, kalau rule-nya ada | Bisa, lewat active scan |
eval() atau fungsi berbahaya lain | Bisa | Hanya kalau bisa dipicu lewat HTTP |
| Password atau API key tertulis di kode | Bisa, lebih tepat pakai secret scanner | Nggak bisa |
| Header keamanan (CSP, HSTS) belum dipasang | Jarang, karena header sering diatur server | Bisa, lewat passive scan |
| Konfigurasi CORS yang terlalu longgar | Kadang, kalau konfigurasinya ada di kode | Bisa |
| Bahasa pemrograman | Harus didukung alatnya | Nggak berpengaruh, yang penting HTTP |
Dari tabel itu kelihatan kenapa keduanya biasanya dipakai bareng. Titik buta yang satu sebagian besar tertutup oleh yang lain.
Tetangga dekat: SCA, secret scanning, dan IAST
Ada tiga istilah lain yang sering muncul bersama SAST dan DAST. SCA (Software Composition Analysis) memeriksa dependency di package.json atau requirements.txt, lalu mencocokkan versinya dengan database CVE. npm audit adalah contoh SCA yang paling sederhana. Secret scanning mencari kredensial yang ikut ter-commit, termasuk yang sudah terkubur di riwayat Git. IAST (Interactive Application Security Testing) memasang agen di dalam aplikasi yang sedang jalan, sehingga request HTTP bisa dikaitkan langsung dengan baris kode yang dieksekusi. IAST butuh agen khusus per bahasa, jadi jarang dipakai di proyek kuliah.
Praktik SAST dengan Semgrep
Semgrep adalah alat SAST open source yang mendukung banyak bahasa, dan rule-nya ditulis dalam YAML dengan pola yang mirip kode aslinya. Dokumentasi resminya menganjurkan install lewat pipx. Di macOS bisa juga lewat Homebrew, walaupun versinya kadang tertinggal.
pipx install semgrep # atau: brew install semgrep
semgrep --versionKalau belum punya pipx, pip install semgrep di dalam virtual environment juga jalan.
Untuk contoh, saya pakai proyek kecil bernama toko-buku, yaitu aplikasi Express dengan beberapa kesalahan yang disengaja: query SQL yang disambung dari input, eval() untuk menghitung diskon, dan secret JWT yang ditulis langsung di kode. Jalankan dari folder proyek:
semgrep scan --config p/owasp-top-ten --config p/javascript .semgrep scan bisa dipakai tanpa akun. Opsi --config menunjuk ruleset dari Semgrep Registry dan boleh diulang beberapa kali. Waktu saya jalankan dengan Semgrep 1.183, hasilnya seperti ini:
![]()
Tiap temuan dibaca dari atas: nama file, ID rule beserta tandanya (❯❯❱ untuk severity tinggi, ❯❱ untuk sedang), penjelasan, link detail, lalu potongan kode dengan nomor barisnya. Link sg.run membuka halaman rule tersebut. Untuk dipakai di CI, tambahkan --json atau --sarif-output=semgrep.sarif supaya hasilnya bisa diolah alat lain.
Ada yang menarik dari hasil itu. Query SQL di books.js justru nggak ditandai, padahal itu bug paling parah di proyek ini, dan itulah contoh false negative. Setelah saya tambahkan --config p/expressjs, rule tainted-sql-string menangkapnya di baris 8. Hasil SAST hanya sebagus ruleset yang dipakai, jadi pilih ruleset yang sesuai dengan framework kamu, dan jangan anggap "0 findings" sebagai bukti aplikasi aman.
Menulis rule sendiri
Kadang tim punya aturan sendiri, misalnya semua query wajib memakai parameter. Rule Semgrep untuk aturan semacam itu cukup ditulis dengan pola yang mirip kode yang dicari. Simpan sebagai .semgrep/rules.yaml:
rules:
- id: sql-string-concat
languages: [javascript, typescript]
severity: ERROR
message: >-
Query SQL disusun dengan menyambung string dari input pengguna.
Pakai parameterized query, misalnya db.query("... WHERE id = ?", [id]).
metadata:
cwe: "CWE-89: SQL Injection"
owasp: "A03:2021 - Injection"
patterns:
- pattern-either:
- pattern: |
$SQL = "$QUERY" + $INPUT + ...;
...
$DB.query($SQL, ...);
- pattern: $DB.query("$QUERY" + $INPUT + ..., ...)
- metavariable-regex:
metavariable: $QUERY
regex: (?i)^\s*(select|insert|update|delete)\b
- id: no-eval
languages: [javascript, typescript]
severity: WARNING
message: eval() menjalankan string sebagai kode. Hampir selalu ada cara yang lebih aman.
pattern: eval(...)Kata yang diawali $ adalah metavariable, yang bisa cocok dengan ekspresi apa pun. Tanda ... berarti "apa saja di antaranya", entah argumen lain atau baris kode lain. pattern-either cukup dipenuhi salah satu polanya, sedangkan metavariable-regex memastikan string-nya memang diawali kata kunci SQL supaya penyambungan string biasa nggak ikut ditandai. Jalankan:
semgrep scan --config .semgrep/rules.yaml --error .Rule pertama menandai baris 8 sampai 12 di books.js, dan rule kedua menandai eval di calc.js. Opsi --error membuat Semgrep keluar dengan exit code 1 kalau ada temuan. Tanpa opsi itu exit code-nya tetap 0 walaupun ada temuan, dan pipeline CI kamu akan tetap hijau.
Dokumentasi Semgrep yang baru menulis severity sebagai LOW sampai CRITICAL, tetapi waktu saya coba, filter --severity ERROR di CLI justru melewatkan rule yang ditulis HIGH. Kalau mau memakai filter itu, tulis severity dengan nama lama dulu.
Secret scanning dengan gitleaks
Password dan API key yang ikut ter-commit itu kasus yang sangat sering, dan menghapusnya di commit berikutnya belum cukup karena isinya masih ada di commit lama. gitleaks khusus mencari pola seperti ini:
brew install gitleaks # atau pakai image ghcr.io/gitleaks/gitleaks
gitleaks git -v --redact . # periksa seluruh riwayat commit
gitleaks dir -v --redact . # periksa isi folder saat ini sajaSejak versi 8.19, perintah gitleaks detect dan gitleaks protect sudah deprecated dan digantikan git serta dir. Opsi --redact menyembunyikan nilai secret di output, jadi log-nya aman dibaca orang lain. Contoh satu temuan:
Finding: mailgunApiKey: "REDACTED"
Secret: REDACTED
RuleID: generic-api-key
Entropy: 4.179229
File: src/config.js
Line: 2
Fingerprint: src/config.js:generic-api-key:2Kalau yang ketemu ternyata secret asli, anggap secret itu sudah bocor. Ganti (rotate) dulu di layanan terkait, baru bersihkan kodenya.
Sebelum DAST: pastikan kamu punya izin
DAST mengirim request dalam jumlah besar, dan active scan mengirim serangan sungguhan. Menjalankannya ke sistem orang lain tanpa izin bisa masuk kategori akses tanpa hak, yang diatur dalam Pasal 30 UU ITE. Karena itu, scan hanya aplikasi milik sendiri, lingkungan lab yang memang disediakan, atau sistem yang pemiliknya sudah memberi izin tertulis beserta batas cakupannya. Website kampus, aplikasi teman, dan demo publik Juice Shop nggak termasuk.
Praktik DAST dengan OWASP ZAP dan Juice Shop
Untuk latihan kita butuh target yang memang dibuat rentan. OWASP Juice Shop adalah toko online tiruan yang sengaja diisi lebih dari seratus tantangan keamanan, dan bisa dijalankan lokal lewat Docker. Supaya ZAP bisa menjangkau Juice Shop lewat nama container, taruh keduanya di satu jaringan Docker:
docker network create lab-keamanan
docker run -d --name juice-shop --network lab-keamanan \
-p 127.0.0.1:3000:3000 bkimminich/juice-shopOpsi -p 127.0.0.1:3000:3000 membuat Juice Shop hanya bisa dibuka dari laptop kamu sendiri di http://localhost:3000. Aplikasi yang sengaja dibuat rentan sebaiknya nggak terbuka ke jaringan Wi-Fi kampus.
Sekarang jalankan ZAP baseline scan dari folder kerja yang masih kosong:
mkdir -p ~/lab-keamanan && cd ~/lab-keamanan
docker run --rm -t --network lab-keamanan \
-v "$(pwd)":/zap/wrk/:rw \
ghcr.io/zaproxy/zaproxy:stable \
zap-baseline.py -t http://juice-shop:3000 -r report.htmlLaporan ditulis ke /zap/wrk di dalam container, jadi folder itu harus di-mount dengan -v, kalau nggak, report.html ikut hilang bersama container. Di dalam container, localhost menunjuk ke container ZAP itu sendiri, karena itu targetnya http://juice-shop:3000, nama container di jaringan yang sama. Kalau aplikasi yang mau diuji jalan langsung di laptop (misalnya lewat npm run dev), pakai http://host.docker.internal:3000 di Docker Desktop. Di Linux, tambahkan --add-host=host.docker.internal:host-gateway dan pastikan aplikasinya listen di 0.0.0.0.
Baseline scan menjalankan spider selama satu menit (bisa diubah dengan -m), menunggu passive scan selesai, lalu melaporkan hasilnya tanpa mengirim serangan. Juice Shop dibangun dengan Angular, jadi banyak halamannya baru muncul setelah JavaScript jalan. Tambahkan -j supaya ZAP juga menjalankan Ajax spider yang memakai browser. Outputnya kurang lebih seperti ini, walaupun daftar dan jumlahnya bisa beda tergantung versi:
![]()
Setiap baris memuat nama rule dan ID di dalam kurung siku, misalnya [10038] untuk header CSP yang belum dipasang. PASS berarti rule itu nggak menemukan masalah, sedangkan WARN-NEW adalah peringatan baru beserta contoh URL-nya. Baris terakhir merangkum jumlahnya. Exit code-nya juga punya arti: 0 kalau bersih, 1 kalau ada FAIL, 2 kalau hanya ada WARN, dan 3 kalau scan-nya sendiri gagal.
Laporan lengkapnya ada di report.html. Di situ setiap alert punya tingkat risiko, URL yang terkena, bukti (evidence), saran perbaikan, dan nomor CWE. Mulai dari risiko tertinggi, lalu untuk tiap alert tanyakan dua hal: apakah ini benar terjadi, dan perbaikannya di kode atau di konfigurasi server. Header CSP dan CORS biasanya cukup dibereskan di konfigurasi header, di Express misalnya lewat middleware helmet.
Baseline atau full scan
Baseline aman dijalankan rutin karena hanya mengamati. Untuk menguji apakah form dan parameter di Juice Shop bisa ditembus SQL injection atau XSS, kamu butuh full scan:
docker run --rm -t --network lab-keamanan \
-v "$(pwd)":/zap/wrk/:rw \
ghcr.io/zaproxy/zaproxy:stable \
zap-full-scan.py -t http://juice-shop:3000 -r full-report.htmlFull scan menjalankan spider tanpa batas waktu, lalu active scan yang benar-benar menyerang. Prosesnya bisa lama, bisa mengisi database dengan data sampah, dan bisa membuat aplikasi melambat. Jalankan hanya ke lingkungan yang boleh rusak, misalnya Juice Shop di laptop atau staging yang bisa di-reset.
Nuclei sebagai alternatif
Nuclei dari ProjectDiscovery bekerja dengan cara lain. Alih-alih menjelajah seluruh aplikasi, Nuclei menjalankan ribuan template YAML yang masing-masing mengecek satu hal spesifik: CVE tertentu, panel admin yang terbuka, folder .git yang bisa diunduh, dan sejenisnya.
docker run --rm --network lab-keamanan projectdiscovery/nuclei \
-u http://juice-shop:3000 -s medium,high,criticalSetiap temuan dicetak satu baris dengan format [template-id] [protokol] [severity] URL. Opsi -as membuat Nuclei mendeteksi teknologi target dulu lalu memilih template yang relevan, dan -rl membatasi jumlah request per detik supaya target nggak kewalahan.
Memasukkan keduanya ke CI
Scanner baru terasa gunanya kalau jalan otomatis. Pola yang umum: SAST jalan di setiap push dan pull request karena cepat, sedangkan DAST jalan terjadwal karena butuh aplikasi yang hidup dan waktunya lebih lama.
![]()
Contoh minimal untuk GitHub Actions, simpan sebagai .github/workflows/security.yml:
name: security
on:
push:
branches: [main]
pull_request:
schedule:
- cron: "0 1 * * 1" # tiap Senin 01:00 UTC
workflow_dispatch:
permissions:
contents: read
jobs:
semgrep:
if: github.event_name == 'push' || github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install Semgrep
run: pipx install semgrep
- name: Scan
run: |
semgrep scan --config p/owasp-top-ten --config p/javascript \
--config .semgrep/rules.yaml --severity ERROR --error .
zap-baseline:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
services:
app:
image: bkimminich/juice-shop # ganti dengan image aplikasi kamu
ports:
- 3000:3000
steps:
- uses: actions/checkout@v5
- name: Tunggu aplikasi siap
run: curl -sf --retry 30 --retry-delay 2 --retry-connrefused http://localhost:3000 > /dev/null
- name: ZAP baseline
uses: zaproxy/action-baseline@v0.15.0
with:
target: http://localhost:3000
rules_file_name: .zap/rules.tsv
cmd_options: '-I'
fail_action: true
allow_issue_writing: falseWorkflow itu punya dua quality gate. Job semgrep gagal kalau ada temuan ERROR, karena --severity ERROR hanya menyisakan temuan berat dan --error mengubahnya jadi exit code 1. Supaya benar-benar menahan merge, jadikan job ini status check yang wajib lulus lewat branch protection atau ruleset di pengaturan repository.
Job zap-baseline sengaja dibuat lebih longgar. Opsi -I membuat peringatan WARN nggak menggagalkan scan, jadi job baru merah kalau ada rule berstatus FAIL. Rule mana yang FAIL ditentukan di .zap/rules.tsv. File awalnya bisa dibuat dengan opsi -g:
docker run --rm -t --network lab-keamanan -v "$(pwd)":/zap/wrk/:rw \
ghcr.io/zaproxy/zaproxy:stable \
zap-baseline.py -t http://juice-shop:3000 -g rules.tsvSemua rule di file itu awalnya WARN. Ubah yang menurut tim wajib dipenuhi menjadi FAIL, dan yang sengaja diterima menjadi IGNORE. Laporannya tersimpan sebagai artifact zap_scan, dan allow_issue_writing: false mencegah action membuat issue baru tiap minggu.
Saran saya, mulai dari gate yang longgar. Kalau sejak hari pertama semua WARNING memblokir merge, tim akan kebanjiran false positive dan tergoda mematikan scanner-nya. Bersihkan dulu temuan yang ada, baru kencangkan gate.
Kapan pakai yang mana
Kalau baru mulai, pasang SAST dan secret scanning dulu. Keduanya cepat, bisa jalan di setiap push, dan nggak butuh server. Semgrep dengan ruleset yang sesuai framework ditambah gitleaks sudah menangkap banyak kesalahan yang paling sering muncul di proyek kuliah.
DAST mulai berguna begitu aplikasimu punya lingkungan yang bisa diakses lewat HTTP, entah staging atau sekadar container di CI. Mulai dari baseline terjadwal, bereskan header dan konfigurasi yang dilaporkan, baru pertimbangkan full scan ke lingkungan yang boleh rusak.
Keduanya juga nggak menggantikan code review. Scanner efektif untuk pola yang sudah dikenal, tetapi kesalahan logika bisnis sering lolos, misalnya pengguna biasa yang bisa membuka data pengguna lain hanya dengan mengganti ID di URL. Kasus seperti itu baru ketemu kalau kamu sendiri memikirkan bagaimana fitur kamu bisa disalahgunakan.
Langkah selanjutnya
- Jalankan
semgrep scan --config p/owasp-top-ten .dangitleaks git -v --redact .di repo tugas besar kamu, lalu putuskan untuk tiap temuan apakah itu bug sungguhan atau false positive. - Jalankan Juice Shop di laptop, coba ZAP baseline lalu full scan, dan bandingkan isi kedua laporannya.
- Tambahkan workflow di atas ke repo kamu, mulai dari job Semgrep saja.
- Baca dokumentasi resminya: Semgrep, ZAP packaged scans, Nuclei, dan OWASP Juice Shop.
Komentar (0)
Login dengan Google untuk berkomentarBelum ada komentar. Jadilah yang pertama!