//BAGIAN 9 DARI 10
Bagian 9: Proses Exception untuk Temuan Security yang Belum Bisa Diperbaiki
· 19 menit baca
English//TUTORIAL · 9 DARI 10 — Secure CI/CD Pipeline dengan Go, GitHub Actions, Vault, dan Dokploy
- 01Bagian 1: Kenapa Saya Bikin Secure CI/CD Pipeline, dan Persiapan Tools-nya
- 02Bagian 2: Vault, Secret, Policy, dan AppRole per Environment
- 03Bagian 3: Aplikasi Go Kecil yang Membaca Secret dari File dan Menolak Jalan Kalau Config Salah
- 04Bagian 4: Vault Agent Sidecar dan Docker Compose, Secret Masuk Tanpa App Tahu Vault Ada
- 05Bagian 5: Repo GitHub dan Workflow PR sebagai Security Gate
- 06Bagian 6: Ruleset yang Memblokir Merge, dan Lima Demo PR yang Harus Gagal
- 07Bagian 7: Menyiapkan Environment Dev dan Production di Dokploy
- 08Bagian 8: Workflow Deploy, Build Sekali, Scan, SBOM, GHCR, lalu Promosi ke Production
- 09Bagian 9: Proses Exception untuk Temuan Security yang Belum Bisa Diperbaiki
- 10Bagian 10: CI Mengambil Token Dokploy dari Vault lewat GitHub OIDC, dan Penutup Seri
Sampai Bagian 8, pipeline kita sudah galak. Ada secret, CVE, misconfig Dockerfile, atau kode berbahaya, PR-nya merah dan nggak bisa di-merge. Deploy juga berhenti kalau image punya CVE CRITICAL yang sudah ada fix-nya.
Masalahnya, dunia nyata nggak serapi itu. Kadang ada CVE di base image yang belum ada patch-nya, padahal fitur harus rilis. Kadang scanner salah paham: string tes tetap di kode dianggap API key. Kalau nggak ada jalur resmi untuk kasus begini, orang akan cari jalan pintas: mematikan scanner, menurunkan severity, atau menambah ignore diam-diam. Begitu itu terjadi, gate tinggal formalitas.
Di bagian ini saya bikin proses exception: cara resmi untuk menerima satu temuan, sementara, dengan nama orang yang bertanggung jawab dan tanggal kapan izinnya habis. Ini juga pertanyaan klasik auditor, apalagi di bank: "Temuan yang nggak diperbaiki ini, siapa yang menyetujui, dan sampai kapan?" Setelah bagian ini, jawabannya ada di Git history.
Konsep dulu: exception yang sehat
Analogi yang saya pakai: surat izin nggak masuk kerja. Ada suratnya, ada alasannya, ada yang tanda tangan, dan berlaku sampai tanggal tertentu. Lewat tanggal itu, kamu dianggap bolos lagi. Bukan izin seumur hidup.
Dari situ saya turunkan empat syarat:
Syarat | Kenapa | Ditegakkan oleh |
|---|---|---|
Lewat PR, tersimpan di repo | Git history jadi audit trail: siapa, kapan, kenapa | Ruleset dari Bagian 6: semua perubahan wajib lewat PR |
Owner + alasan | Ada orang yang bertanggung jawab memantau |
|
Kedaluwarsa maksimal 90 hari | Risiko diterima sementara. Setelah lewat, perbaiki atau perpanjang dengan alasan baru | Script + |
Sempit: 1 CVE, 1 fingerprint, 1 rule di 1 baris | Exception lebar (seluruh rule, seluruh folder) bisa menyembunyikan temuan baru yang belum pernah dinilai | Script melarang suppression yang lebar |
Bentuk akhirnya seperti ini:
Temuan scanner di PR │ ├─ bisa diperbaiki? ──► perbaiki. Selesai. │ └─ belum bisa (belum ada patch / false positive) │ ▼ PR exception: 1 entry = 1 temuan, plus baris di atasnya: exception: owner=@user exp=YYYY-MM-DD reason="..." │ ▼ check "exceptions" (scripts/check-exceptions.sh), wajib di ruleset ✗ tanpa owner/alasan/tanggal ✗ sudah kedaluwarsa ✗ lebih dari 90 hari ✗ nosemgrep tanpa rule-id ✗ gitleaks:allow, .semgrepignore, .gitleaks.toml, trivy.yaml │ ▼ merge → tercatat di Git history │ ▼ tiap Senin: workflow "exception-expiry" → kedaluwarsa? merah Trivy juga melapor lagi sendiri setelah exp:Kenapa butuh script sendiri
Awalnya saya pikir cukup pakai fitur bawaan scanner. Setelah saya coba satu per satu di lokal, ternyata nggak:
Scanner | Cara suppress | Punya tanggal kedaluwarsa? |
|---|---|---|
Trivy |
| Ya. Setelah |
Semgrep |
| Nggak |
Gitleaks | Fingerprint di | Nggak |
Yang sempat bikin saya kaget: nosemgrep tanpa rule-id juga diterima Semgrep, dan itu mematikan semua rule di baris tersebut. Jadi syarat "owner, alasan, tanggal" untuk Semgrep dan Gitleaks cuma bisa ditegakkan oleh check tambahan. Sekalian, script-nya menyamakan format exception untuk semua scanner.
Catatan
govulncheck dan dependency-review sengaja tanpa exception
govulncheck hanya melapor kalau kode kamu benar-benar memanggil fungsi yang rentan. Itu risiko nyata: upgrade, atau berhenti memanggilnya. dependency-review hanya mengecek dependency yang ditambahkan PR, jadi solusinya pilih versi lain. Nggak semua gate harus bisa dikecualikan.
Kebijakannya ditulis dulu
Aturan yang nggak tertulis itu cuma kebiasaan. Saya tulis kebijakannya di docs/exception-process.md, dalam bahasa Inggris karena repo-nya publik. Ini versi yang dipangkas, bagian contoh dan batasan saya lewati:
# Security finding exceptions The PR gates block a merge on any finding. Sometimes a finding has to be accepted for a while: a CVEwith no fixed version yet, or a scanner false positive. This is the only allowed way to do that. ## Rules 1. **Fix first.** An exception is for a finding that cannot be fixed now, not one that is inconvenient.2. **Through a PR.** Exceptions live in the repo, so Git history records who added one, when and why. The PR description links an issue with the details.3. **One finding per entry.** One CVE ID, one Gitleaks fingerprint, or one `nosemgrep` with a rule id. Never a whole rule, a whole severity, or a whole path.4. **Owner, reason and expiry.** Every entry has this line directly above it: ``` exception: owner=@github-user exp=YYYY-MM-DD reason="at least 10 characters, link to the issue" ``` `exp` is at most **90 days** away. To keep an exception longer, renew it in a new PR with a fresh reason.5. **Real secrets are never excepted.** A Gitleaks finding that is a real credential is revoked and rotated. Gitleaks exceptions are for false positives only. ## Per scanner | Scanner | Where | Entry || --- | --- | --- || Trivy (image, config) | `.trivyignore` | `CVE-2026-12345 exp:2026-12-31` or `DS-0002 exp:2026-12-31` || Gitleaks | `.gitleaksignore` | Fingerprint `<commit>:<file>:<rule-id>:<line>` || Semgrep | the source line | `// nosemgrep: <rule-id>`, with the exception line directly above || govulncheck | none | No suppression || dependency-review | none | No suppression | Not allowed, because they suppress findings without an owner or expiry: `.semgrepignore`, `.gitleaks.toml`,`.trivyignore.yaml`, `trivy.yaml`, a bare `nosemgrep`, and `gitleaks:allow`.Aturan nomor 5 itu yang paling penting. Kalau Gitleaks menemukan secret asli, jawabannya bukan exception. Cabut, rotasi, selesai. Exception Gitleaks hanya untuk false positive.
File exception resmi
Dua file ini saya buat sekarang, walau masih kosong, isinya cuma header komentar. Alasannya praktis: workflow nanti menunjuk .trivyignore secara eksplisit, dan kalau file-nya nggak ada, trivy-action berhenti dengan error cannot find ignorefile.
# Trivy exceptions (image CVEs and config checks). Rules: docs/exception-process.md# Each entry needs this line directly above it, and the same date as Trivy's exp:# # exception: owner=@github-user exp=YYYY-MM-DD reason="why it is accepted, link to the issue"# CVE-YYYY-NNNNN exp:YYYY-MM-DD# Gitleaks exceptions, for false positives only. A real leaked secret is rotated, never ignored.# Rules: docs/exception-process.md. Each fingerprint needs this line directly above it:# # exception: owner=@github-user exp=YYYY-MM-DD reason="why it is not a secret, link to the issue"# <commit>:<file>:<rule-id>:<line>Baris yang diawali # dianggap komentar oleh Trivy dan Gitleaks. Sudah saya uji, jadi header ini nggak mengecualikan apa pun.
Penegaknya: scripts/check-exceptions.sh
Ini inti bagian ini. Script-nya membaca semua tempat suppression bisa bersembunyi, lalu menolak yang nggak memenuhi aturan. Saya tampilkan utuh karena setiap bagiannya punya alasan:
#!/usr/bin/env bash# Enforces docs/exception-process.md: every scanner suppression carries an owner, a reason and an expiry# no more than MAX_DAYS away. Run from the repo root. Exits 1 on any violation.set -euo pipefail MAX_DAYS="${MAX_DAYS:-90}"WARN_DAYS="${WARN_DAYS:-14}"TODAY="${TODAY:-$(date -u +%F)}"META_RE='exception: owner=@[A-Za-z0-9-]+ exp=([0-9]{4}-[0-9]{2}-[0-9]{2}) reason="[^"]{10,}"'NOSEM_RE='nosemgrep: ?[A-Za-z0-9_.-]+'errors=0 fail() { echo "::error file=$1,line=$2::$3"; errors=$((errors + 1)); } # GNU date on the runner, BSD date on macOS. valid_date prints nothing for an impossible date.valid_date() { date -u -d "$1" +%F 2>/dev/null || date -u -j -f %F "$1" +%F 2>/dev/null || true; }add_days() { date -u -d "$1 + $2 days" +%F 2>/dev/null || date -u -j -v+"$2"d -f %F "$1" +%F; } LIMIT="$(add_days "$TODAY" "$MAX_DAYS")"WARN_FROM="$(add_days "$TODAY" "$WARN_DAYS")" # check_meta FILE LINE TEXT: TEXT is the line directly above the suppression. Sets $exp when it is valid.check_meta() { exp="" if [[ ! "$3" =~ $META_RE ]]; then fail "$1" "$2" "suppression needs 'exception: owner=@user exp=YYYY-MM-DD reason=\"...\"' on the line directly above" return 1 fi exp="${BASH_REMATCH[1]}" if [ "$(valid_date "$exp")" != "$exp" ]; then fail "$1" "$2" "exp=$exp is not a real date" return 1 elif [[ "$exp" < "$TODAY" ]]; then fail "$1" "$2" "exception expired on $exp: fix the finding or renew it in a new PR" return 1 elif [[ "$exp" > "$LIMIT" ]]; then fail "$1" "$2" "exp=$exp is more than $MAX_DAYS days away (latest allowed $LIMIT)" return 1 elif [[ "$exp" < "$WARN_FROM" ]]; then echo "::warning file=$1,line=$2::exception expires on $exp" fi} # Plain ignore files: every entry needs the exception line directly above it.# shellcheck disable=SC2094 # fail only prints the file name, it never writes to the filecheck_ignore_file() { local file="$1" n=0 prev="" line [ -f "$file" ] || return 0 while IFS= read -r line || [ -n "$line" ]; do n=$((n + 1)) case "$line" in '' | '#'*) prev="$line"; continue ;; esac # Trivy only re-reports a finding after its own exp: date, so it must match the exception line. if check_meta "$file" "$n" "$prev" && [ "$file" = .trivyignore ] && [[ "$line" != *" exp:$exp" ]]; then fail "$file" "$n" "Trivy entry must end with ' exp:$exp'" fi prev="$line" done < "$file"} # These files suppress findings silently, with no owner or expiry, so they are not allowed at all.for f in .semgrepignore .gitleaks.toml .trivyignore.yaml trivy.yaml; do if [ -e "$f" ]; then fail "$f" 1 "not allowed: use the exception process in docs/exception-process.md" fidone check_ignore_file .trivyignorecheck_ignore_file .gitleaksignore # Inline suppressions. gitleaks:allow is banned because a .gitleaksignore fingerprint pins one finding, not a whole line.while IFS=: read -r file n text; do if [[ "$text" == *gitleaks:allow* ]]; then fail "$file" "$n" "gitleaks:allow is not allowed: add the finding's fingerprint to .gitleaksignore" continue fi if [[ ! "$text" =~ $NOSEM_RE ]]; then fail "$file" "$n" "nosemgrep must name the rule id (nosemgrep: <rule-id>)" continue fi prev="" [ "$n" -gt 1 ] && prev="$(sed -n "$((n - 1))p" "$file")" check_meta "$file" "$n" "$prev" || truedone < <(git grep -n -I -E 'nosemgrep|gitleaks:allow' -- . ':!*.md' ':!scripts/check-exceptions.sh' || true) if [ "$errors" -gt 0 ]; then echo "$errors exception problem(s). See docs/exception-process.md" >&2 exit 1fiecho "exceptions OK (today $TODAY, latest allowed exp $LIMIT)"Bagian-bagian yang penting:
Bagian | Fungsinya |
|---|---|
| Regex baris exception: |
| Mencetak |
| Coba |
| Perbandingan teks. Format |
Peringatan 14 hari | Kalau sisa kurang dari 14 hari, keluar |
Khusus | Entry wajib diakhiri |
Daftar file terlarang |
|
| Cari |
Kenapa gitleaks:allow dilarang padahal itu fitur resmi Gitleaks? Karena komentar itu mengabaikan satu baris, untuk selamanya, tanpa peduli isinya berubah apa. Fingerprint di .gitleaksignore jauh lebih sempit: satu temuan di satu commit.
Perhatian
`git grep` hanya melihat file yang di-track
File baru yang belum di-git add nggak ikut diperiksa. Di CI semua sudah ter-commit, jadi aman. Waktu uji lokal, git add dulu. Saya sempat bingung kenapa lokal exceptions OK tapi CI merah, ternyata ini penyebabnya.
Cek hasil: jalankan di root repo.
chmod +x scripts/check-exceptions.shgit add -Abash scripts/check-exceptions.shexceptions OK (today 2026-10-31, latest allowed exp 2027-01-29)Lalu uji negatif singkat: nosemgrep polos harus ditolak.
printf 'package demo\n\nconst a = "x" // nosemgrep\n' > internal/tmp_test_nosem.gogit add internal/tmp_test_nosem.gobash scripts/check-exceptions.sh; echo "exit=$?"git rm -q --cached internal/tmp_test_nosem.go && rm internal/tmp_test_nosem.go::error file=internal/tmp_test_nosem.go,line=3::nosemgrep must name the rule id (nosemgrep: <rule-id>)1 exception problem(s). See docs/exception-process.mdexit=1Selain ini saya jalankan 24 kasus uji lain: tanpa baris exception, baris kosong di antara, tanggal Trivy beda, exp hari ini, tepat 90 hari (lolos), 91 hari (ditolak), alasan terlalu pendek, gitleaks:allow, .semgrepignore, dan seterusnya. Semuanya lulus di macOS (bash 3.2) dan Ubuntu (bash 5.2, sama seperti runner GitHub). Script-nya juga lolos shellcheck.
Menyambungkan ke workflow
Job exceptions di PR
Di pr-checks.yml, saya tambah satu job di antara semgrep dan govulncheck:
exceptions: runs-on: ubuntu-24.04 timeout-minutes: 5 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Check scanner exceptions run: bash scripts/check-exceptions.shJob ini nggak butuh permission tambahan, cukup contents: read dari level workflow.
Trivy membaca .trivyignore secara eksplisit
Trivy sebenarnya otomatis membaca .trivyignore. Tapi saya tetap menambahkan trivyignores: .trivyignore di kelima step trivy-action: empat di pr-checks.yml (config SARIF, config gate, image SARIF, image gate) dan satu di gate deploy.yml.
- name: Trivy image (gate) uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: version: v0.76.1 trivyignores: .trivyignore scan-type: image # ...sisanya sama seperti Bagian 5Ada tiga alasan:
Kalau
trivyignoresdiisi, action mencetak isi file ignore ke log (Found ignorefile '.trivyignore':). Setiap run jadi menunjukkan exception apa saja yang aktif. Bagus untuk audit.Kalau file-nya hilang, action gagal, bukan diam-diam scan tanpa exception.
Gate di
deploy.ymlmembaca file yang sama. Exception yang disetujui di PR juga berlaku saat build deploy. Satu sumber kebenaran.
Cek mingguan exception-expiry
PR check hanya jalan kalau ada PR. Kalau dua bulan nggak ada PR, exception yang kedaluwarsa nggak ketahuan. Jadi saya tambah workflow terjadwal:
name: exception-expiry # PR checks only run when someone opens a PR. This weekly run catches exceptions that expire in between,# and GitHub emails the failure.on: schedule: - cron: "0 1 * * 1" # Monday 01:00 UTC = 08:00 WIB workflow_dispatch: permissions: contents: read jobs: exception-expiry: runs-on: ubuntu-24.04 timeout-minutes: 5 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Check scanner exceptions run: bash scripts/check-exceptions.shBagian | Artinya |
|---|---|
| Menit 0, jam 01 UTC, setiap Senin. Itu 08:00 WIB |
| Bisa dijalankan manual: |
Branch yang dicek | Workflow terjadwal selalu jalan di default branch, yaitu |
Notifikasi | Kalau gagal, GitHub mengirim email ke orang yang terakhir mengubah cron ini |
Nama job | Sengaja beda dari |
Catatan
Jadwal GitHub bisa telat beberapa menit sampai satu jam waktu sibuk, dan dinonaktifkan kalau repo sepi 60 hari. Untuk cek mingguan, itu masih bisa diterima, karena Trivy exp: dan check exceptions tetap jalan di setiap PR dan deploy.
Jadikan exceptions required check
Di docs/rulesets/protect-development.json dan protect-main.json, saya tambahkan exceptions ke daftar required check, lalu terapkan dengan gh api -X PUT seperti di Bagian 6. Cek hasil:
for f in docs/rulesets/protect-development.json docs/rulesets/protect-main.json; do jq -c '[.rules[] | select(.type=="required_status_checks").parameters.required_status_checks[].context]' "$f"done["test","gitleaks","semgrep","govulncheck","dependency-review","trivy-config","trivy-image","exceptions"]["test","gitleaks","semgrep","govulncheck","dependency-review","trivy-config","trivy-image","exceptions","source-branch"]Perhatian
Terapkan ruleset setelah merge, bukan sebelum
Kalau exceptions dijadikan wajib sebelum job-nya ada di development, PR lain yang dibuat dari development lama nggak pernah melaporkan check itu. PR-nya akan menunggu selamanya dengan status "Expected — Waiting for status to be reported". Jadi urutannya: merge PR yang menambah job, baru PUT ruleset.
PR yang menambah semua ini (9 file) saya buka ke development. Yang muncul 7 gate lama ditambah exceptions, semuanya hijau, lalu merge. Setelah itu baru ruleset-nya diterapkan.
Demo A: accepted risk yang lalu kedaluwarsa
Sekarang buktinya. Skenario: container terpaksa jalan sebagai root, misalnya aplikasi lama yang butuh port 80. Temuan ini ditangkap dua scanner: Trivy config (DS-0002) dan Semgrep (dockerfile.security.last-user-is-root.last-user-is-root). Satu temuan berarti exception di kedua scanner. Sengaja repot, supaya exception jarang dipakai.
PR ini demo, nanti ditutup tanpa merge.
git switch development && git pull --ff-onlygit switch -c demo/exception-accepted-riskEXP=$(date -v+30d +%F); echo "$EXP" # macOS. Di Linux: date -d '+30 days' +%FDi Dockerfile, baris USER 65532:65532 saya ganti dengan tiga baris ini (ganti <EXP> dengan tanggal dari perintah di atas):
# exception: owner=@dimss113 exp=<EXP> reason="demo: root needed to bind port 80, tracked in issue"# nosemgrep: dockerfile.security.last-user-is-root.last-user-is-rootUSER rootUrutannya penting: baris exception: langsung di atas nosemgrep, dan nosemgrep langsung di atas USER root.
Perhatian
Error `suppression needs 'exception: ...'` di Dockerfile
Ini yang sempat kena ke saya. Hampir pasti <EXP> belum diganti. Script hanya menerima tanggal berformat YYYY-MM-DD, jadi exp=<EXP> nggak dianggap baris exception. Jalankan sed -i '' "s/exp=<EXP>/exp=$EXP/" Dockerfile. Kalau terminalnya baru dan $EXP kosong, isi dulu variabelnya.
Lalu entry Trivy, dengan tanggal yang sama:
printf '# exception: owner=@dimss113 exp=%s reason="demo: root needed to bind port 80, tracked in issue"\nDS-0002 exp:%s\n' "$EXP" "$EXP" >> .trivyignoregit add -A && bash scripts/check-exceptions.sh # harus: exceptions OKgit commit -m "demo: accept root user with an expiring exception"git push -u origin demo/exception-accepted-riskgh pr create --base development --title "demo: accepted risk with expiry" \ --body "Demo only, not to be merged."gh pr checks --watchCek hasil: kedelapan required check hijau, termasuk trivy-config, semgrep, dan exceptions. PR-nya bisa di-merge.
Ada satu hal yang awalnya bikin saya bingung: check Code scanning results / Semgrep OSS tetap merah. Itu bukan job semgrep kita. Itu check terpisah yang dibuat GitHub dari file SARIF. Semgrep tetap menulis temuan yang di-nosemgrep ke SARIF dengan tanda suppressions: inSource, tapi code scanning GitHub mengabaikan tanda itu dan membuka alert baru. Check ini nggak ada di ruleset, jadi nggak memblokir:
gh pr view --json mergeStateStatus,mergeable{"mergeStateStatus":"UNSTABLE","mergeable":"MERGEABLE"}UNSTABLE artinya ada check opsional yang merah, tapi tetap bisa merge. Justru bagus untuk audit: exception tetap terlihat di tab Security. Untuk exception sungguhan, alert-nya ditutup lewat Dismiss alert → Won't fix dengan alasan dan tanggal yang sama, jadi keputusan itu juga tercatat di GitHub, lengkap dengan siapa yang memutuskan.
Kedaluwarsa = merah lagi
Sekarang tanggalnya saya mundurkan ke kemarin, di dua tempat sekaligus:
OLD=$(date -v-1d +%F)sed -i '' "s/$EXP/$OLD/g" Dockerfile .trivyignoregit commit -am "demo: let the exception expire"git pushgh pr checks --watchCek hasil:
Check | Status | Kenapa |
|---|---|---|
| merah |
|
| merah | Trivy sendiri berhenti mengabaikan |
| hijau | Semgrep nggak kenal tanggal. Tanpa check |
Baris semgrep hijau itu adalah bukti paling jelas kenapa check exceptions dibutuhkan. Setelah itu PR-nya saya tutup:
gh pr close --delete-branch --comment "Demo only: exception accepted, then expired and blocked."Demo B: false positive Gitleaks lewat fingerprint
Skenario kedua: ada nilai tes tetap di kode yang bentuknya mirip API key. Gitleaks dan Semgrep (p/secrets) menandainya. Karena ini false positive, boleh di-exception.
git switch development && git pull --ff-onlygit switch -c demo/exception-false-positivemkdir -p internal/demoV=$(openssl rand -hex 20)printf 'package demo\n\n// Demo only: a fixed test value that looks like an API key.\nconst apiKey = "%s"\n\nfunc Key() string { return apiKey }\n' "$V" > internal/demo/vector.gounset Vgit add -A && git commit -m "demo: add a fixed test value that looks like an api key"openssl rand -hex 20 menghasilkan 40 karakter hex acak, jelas bukan kredensial siapa pun.
Ambil fingerprint
gitleaks git . --no-banner --redact -v --log-opts="origin/development..HEAD"--log-opts="origin/development..HEAD" membuat Gitleaks hanya memindai commit di branch ini, sama seperti CI yang memindai commit PR saja. -v mencetak detail termasuk fingerprint:
RuleID: generic-api-key...Fingerprint: <commit-sha>:internal/demo/vector.go:generic-api-key:4Format fingerprint: <commit>:<file>:<rule>:<baris>. Artinya exception ini hanya berlaku untuk satu temuan di satu commit. Kalau nilai yang sama ditempel lagi di commit lain, Gitleaks tetap menangkapnya. Itu yang saya mau.
Simpan ke variabel supaya nggak perlu copy manual:
FP=$(gitleaks git . --no-banner --redact --log-opts="origin/development..HEAD" \ --report-format json --report-path /tmp/gl.json >/dev/null 2>&1; jq -r '.[0].Fingerprint' /tmp/gl.json)rm /tmp/gl.jsonTambah exception Gitleaks dan Semgrep
EXP=$(date -v+30d +%F)printf '# exception: owner=@dimss113 exp=%s reason="demo: fixed test value, not a real credential"\n%s\n' "$EXP" "$FP" >> .gitleaksignoreUntuk Semgrep, dua baris ditambahkan di atas const apiKey:
// exception: owner=@dimss113 exp=<EXP> reason="demo: fixed test value, not a real credential"// nosemgrep: generic.secrets.security.detected-generic-api-key.detected-generic-api-keyconst apiKey = "..."Perhatian
Jangan taruh `nosemgrep` di ujung baris `const`
Kalau baris const diubah, commit baru ikut menambahkan ulang baris berisi nilai itu. Gitleaks melihatnya sebagai temuan baru dengan SHA commit baru, dan fingerprint tadi nggak cocok lagi. Taruh di baris terpisah di atasnya. Semgrep tetap mengenali nosemgrep di baris atas, sudah saya uji. Hal yang sama berlaku kalau kamu amend atau rebase: SHA berubah, ambil fingerprint lagi.
gofmt -l internal/demo # harus kosonggit add -A && bash scripts/check-exceptions.shgit commit -m "demo: except the false positive by fingerprint"git push -u origin demo/exception-false-positivegh pr create --base development --title "demo: false positive exception" --body "Demo only, not to be merged."gh pr checks --watchCek hasil: semua required check hijau, termasuk gitleaks, semgrep, dan exceptions. Code scanning / Semgrep OSS merah dengan alasan yang sama seperti Demo A. Code scanning / gitleaks tetap hijau, karena temuan yang cocok dengan fingerprint nggak ditulis ke SARIF.
Tips
Mau screenshot "sebelum dan sesudah"? Push commit pertama saja dulu, lihat gitleaks dan semgrep merah, baru tambah exception-nya.
Tutup demo-nya dengan gh pr close --delete-branch.
Kalau ada masalah
Gejala | Penyebab | Solusi |
|---|---|---|
Trivy gagal: |
|
|
| Baris exception nggak tepat di atas, alasan kurang dari 10 karakter, atau tanpa | Taruh tepat di atas, format persis |
| Tanggal entry Trivy beda dengan baris exception | Samakan tanggalnya |
|
| Maksimal 90 hari, perpanjang lewat PR baru |
| Commit di-amend/rebase, atau baris berisi nilai ikut diubah | Ambil fingerprint lagi dari commit terakhir |
| Rule-id salah ketik | Ambil rule-id persis dari log Semgrep |
Batasan yang saya terima
Proses ini nggak sempurna, dan lebih baik ditulis jujur:
Owner bisa diisi siapa saja. Script hanya mengecek format, bukan apakah orang itu setuju. Di tim, pasang
CODEOWNERSuntuk.trivyignore,.gitleaksignore, script, dan workflow, plus "require code owner review" di ruleset.Penulis PR bisa mengubah script atau workflow di PR yang sama, karena
pull_requestmemakai workflow dari branch PR. Mitigasinya review ketat untuk.github/danscripts/, atau required workflows di level organisasi.Fingerprint Gitleaks tetap menyembunyikan satu temuan lama setelah kedaluwarsa, karena Gitleaks nggak punya tanggal. Check
exceptionstetap merah sampai entry-nya dihapus, dan fingerprint cuma cocok untuk satu commit.Belum ada dashboard terpusat. Exception tersebar di tiga tempat, tapi
git grep -n "exception:"menampilkan semuanya.
Rangkuman
docs/exception-process.md: kebijakan tertulis. Perbaiki dulu, satu temuan per entry, owner + alasan + kedaluwarsa maksimal 90 hari, secret asli nggak pernah di-exception..trivyignoredan.gitleaksignoresebagai satu-satunya file exception resmi.scripts/check-exceptions.shyang menolak exception tanpa metadata, kedaluwarsa, terlalu panjang,nosemgreppolos,gitleaks:allow, dan file suppression lebar.Job
exceptionssebagai required check didevelopmentdanmain, plus workflow mingguanexception-expiry.Bukti: exception sah membuat PR hijau, dan begitu kedaluwarsa, gate merah lagi. Termasuk bukti bahwa Semgrep sendiri nggak akan pernah merah lagi tanpa check ini.
Di Bagian 10, bagian terakhir, kita bereskan satu-satunya kredensial yang masih tersimpan di GitHub: DOKPLOY_API_KEY. Secret itu saya hapus, dan CI mengambil token Dokploy langsung dari Vault lewat GitHub OIDC.