Lewati ke konten

Shield Corev1.2.x

Arsitektur

Shield Core punya satu entry point: ShieldEngine::inspect(). Semua yang lain mendukung fungsi itu. Halaman ini menjelaskan apa yang terjadi di dalamnya, dan urutan kejadian itu penting.

RequestContext
│
▼
┌───────────────────────────────────────────────┐
│ ShieldEngine::inspect() │
│ │
│ 1. Normalizer → NormalizedRequest │
│ 2. ThreatSignatureEngine → RuleMatch[] │
│ 3. allowlist? ── ya, keluar di sini │
│ 4. validasi cookie tepercaya │
│ 5. muat ban aktif → BanRecord │
│ 6. gagal? → fail_mode │
│ 7. RiskDecay::decayOffenseCount │
│ 8. BehaviorDetector → BehaviorReport │
│ 9. RiskScorer → ScoreBreakdown │
│ 10. DecisionEngine → Verdict │
│ 11. penyesuaian mode bot │
│ 12. persist: log + tegakkan ban │
│ │
│ ▼ │
│ EngineResult │
└───────────────────────────────────────────────┘

Tidak ada satu pun matcher yang melihat URI mentah. Normalizer memecah request, mendekode ulang, menyamakan huruf besar-kecil, lalu menghasilkan NormalizedRequest. Ini alasannya /%2e%65nv harus dibaca sebagai /.env.

Semua pencocokan berikutnya bekerja pada bentuk yang sudah rapi. Detailnya ada di Konfigurasi bagian normalisasi.

Rule dicocokkan lebih dulu karena biayanya murah dan hasilnya pasti. Kalau sudah ada rule critical yang cocok, hitungan perilaku tidak akan mengubah keputusan itu.

3. Allowlist keluar lebih awal — dengan satu pengecualian

Section titled “3. Allowlist keluar lebih awal — dengan satu pengecualian”

Kalau request berada di allowlist.paths atau allowlist.hosts, engine mengembalikan ALLOW seketika dengan skor 0. Tidak ada query database, tidak ada scoring.

Tapi bukan kalau ada rule critical. Baris kodenya secara eksplisit meminta hasCriticalMatch() lebih dulu. Artinya allowlist tidak bisa dipakai untuk membuka /.env — cacat konfigurasi yang sangat mudah terjadi.

if ($this->isAllowlisted($request) && ! $hasCritical) {
// keluar di sini
}
Section titled “4. Cookie tepercaya dibaca, bukan diberikan”

Engine hanya memvalidasi cookie kalau trusted.enabled aktif dan cookie itu memang dikirim. Nilai cookie diteruskan ke adapter lewat $options; engine tidak membacanya sendiri dari $_COOKIE. Itu keputusan sadar supaya test tidak perlu memanipulasi superglobal.

$result = $engine->inspect($context, $counters, [
'request_id' => $requestId,
'trusted_cookie' => $cookieValue,
]);

loadActiveBan() dipanggil setelah validasi cookie, bukan setelah decision. Kalau BanRepository gagal, variabel $degraded ikut terisi — dan itu yang dipakai langkah berikutnya.

Kalau $degraded dan fail_mode adalah fail_closed, engine memblokir dengan skor 99 dan alasan fail_closed.

Default-nya fail_open: request diteruskan. Alasannya praktis — errornya mematikan seluruh situs dan sulit didiagnosis kalau tidak ada yang bisa diakses untuk membaca log.

Pilihan ini ada karena beberapa adapter lebih memilih memblokir daripada melewatkan permintaan tanpa pemeriksaan sama sekali.

offense_count pada ban aktif dihapus gradually berdasarkan usianya sebelum dipakai untuk scoring. Ban yang sudah lama tidak langsung kembali ke angka nol, dan ini disengaja.

Dua langkah terpisah, dan urutannya tidak dapat ditukar: BehaviorDetector menghasilkan BehaviorReport (sinyal mentah), lalu RiskScorer mengubah sinyal itu plus signature plus riwayat menjadi satu angka.

Kalau Anda mengganti salah satunya, jangan gabungkan. Detailnya ada di Deteksi Perilaku dan Skor & Keputusan.

DecisionEngine menerima DecisionInput lengkap dan mengembalikan Verdict yang punya dua lapis:

Properti Arti
intended Apa yang seharusnya terjadi menurut skor.
decision Apa yang benar-benar terjadi setelah mode diterapkan.

Dua lapis inilah yang membuat observe bekerja tanpa mengubah kode pemanggil. Ketika observe aktif, intended masih TEMP_BAN, tapi decision turun jadi OBSERVE.

Setelah keputusan diambil, ada satu koreksi khusus untuk crawler. Ini menyangkut klaim bot yang tidak bisa diverifikasi:

  • bot_mode = challenge → hasilnya CHALLENGE, tapi hanya kalau keputusan sebelumnya bukan terminal. Ban aktif tidak pernah diturunkan jadi challenge.
  • bot_mode = observe → hasilnya turun ke OBSERVE, tapi hanya kalau tidak ada ban aktif dan tidak ada rule yang cocok.

Pengecualian itu penting. Tanpa itu, crawler sah yang kebetulan gagal reverse-DNS akan tersingkir dari hasil pencarian hanya karena jumlah URI yang dikunjunginya naik. Shield sengaja tidak mau melakukan itu — hilangnya Googlebot dari indeks lebih sulit dideteksi daripada beberapa request yang lolos.

Terakhir, setelah keputusan final. Dua hal yang mungkin terjadi:

  • Log, sesuai logging.level. Default hanya menyimpan keputusan yang bukan ALLOW biasa. Mencatat semua request membuat tabel kejadian tumbuh tanpa batas dan justru menutupi baris yang Anda butuhkan.
  • Tegakkan ban. Kalau keputusan memblokir, ban baru ditulis. Kalau ban aktif tapi request ini diizinkan, ban-nya hanya disentuh (touchLastSeen), diperpanjang, atau dilepas — tergantung aturan. Riwayat tidak pernah dihapus.

EngineResult adalah value object yang immutable. Yang paling sering Anda periksa:

$result->verdict->decision; // apa yang harus dilakukan
$result->verdict->reason; // kode alasan yang bisa disimpan
$result->score->total; // hasil penjumlahan, tanpa plafon
$result->score->matchedRuleIds; // rule yang cocok
$result->score->behaviorSignals; // sinyal perilaku
$result->matches; // detail setiap kecocokan
$result->activeBan; // ban yang berlaku, kalau ada
$result->trusted; // apakah pengenal valid
$result->infrastructureDegraded; // apakah ada contract yang gagal
Method Gunanya
releaseBan($ip, $reason, $actor) Melepas ban aktif. Mengembalikan false kalau tidak ada ban aktif atau BanRepository null.
markChallengePassed($ip) Melepas ban dan mencatat waktu kelulusan challenge.
issueTrustedCookie($request) Membuat nilai cookie tepercaya baru.
trustedCookieName() Nama cookie untuk header Set-Cookie. Default shield_trusted.

markChallengePassed tidak menghapus riwayat. Ban yang dilepas tetap ada di tabel dengan penanda, supaya operator bisa melihat orang yang sama pernah diblokir tiga kali bulan ini.

Beberapa hal sengaja tidak ada di Core, dan itu bukan kelemahan:

Urusan Dimana
Pembacaan $_SERVER, getallheaders(), php://input Adapter
Cache, database, antrian Adapter
Halaman HTML challenge Adapter
Perintah CLI Adapter
Pengiriman notifikasi Adapter

Alasannya sederhana: semuanya berbeda tiap framework, dan bagian yang perlu tetap sama persis antar framework. Daftar rule, scoring, dan urutan keputusan itulah intinya.

Batas Default
thresholds.challenge 10
thresholds.ban 20
thresholds.strong_ban 30
escalation.step 5
ban.durations 15, 60, 360, 1440 menit
performance.ban_cache_ttl_seconds 30
response_code 404

Skor tidak dibatasi di 100. Totalnya adalah penjumlahan sederhana:

$total = $signatureScore
+ $behavior->totalDelta
+ $escalationScore
+ $challengePenalty;

Karena tidak ada plafon, satu request bisa langsung melewati strong_ban. Itu memang perilaku yang diinginkan — tidak perlu menumpuk banyak sinyal untuk memblokir request yang sudah jelas berbahaya.

Skor eskalasi dari riwayat offense adalah bagian yang sering terlewat:

$escalationScore = min($offenseCount, 5) * $config->escalationStep;

Jadi offense ke-1 menambah 5, ke-2 menambah 10, dan berhenti di 25 poin setelah ke-5. Plafon itu ada supaya IP yang sudah pernah beberapa kali diblokir tidak perlu menunggu skor tinggi lagi untuk ban berikutnya.

Nilai standarnya bisa Anda ubah. Yang tidak bisa diubah adalah urutannya: challenge selalu sebelum ban, ban selalu sebelum ban kuat, dan observe selalu menurunkan, tidak pernah menaikkan.

Powered by PT Ganadev Multi Solusi