Gupa adalah lapisan keamanan komprehensif yang memberi skor setiap request secara real-time via multi-detektor, memblokir IP jahat (sementara/permanen), dan menyediakan CLI untuk monitoring/manajemen.
php artisan boost:add-skill bale/gupa
Save this content to: AGENTS.md
# Bale Gupa — Agent Context
> Package: `bale/gupa` | Namespace: `Bale\Gupa` | Brand: Bale ecosystem
>
> Perlindungan IP, rate limiting, dan deteksi bot untuk aplikasi Laravel 11/12/13.
---
## Ringkasan
Gupa adalah lapisan keamanan komprehensif yang memberi skor setiap request secara real-time via multi-detektor, memblokir IP jahat (sementara/permanen), dan menyediakan CLI untuk monitoring/manajemen.
## Cara Kerja
```
Request → GuardianMiddleware::handle()
│
├─ Disabled? → next
├─ Whitelisted? → next
├─ Blacklisted? → 403
├─ Already blocked? → 403
├─ Pending block? → apply → 403
├─ Rate limited? → 429
│
└─ next(request) → response
│
└─ GuardianMiddleware::terminate()
│
├─ Whitelisted/Blocked? → skip
├─ 404? → recordNotFound()
└─ processScoring()
│
├─ ScoreCalculator::calculate()
│ └─ iterates 5 detectors → sum score
│
├─ ScoreCalculator::increment() → cache
│
├─ LogAction::logSuspiciousRequest()
│
└─ If score >= threshold → BlockAction::execute()
├─ whitelistChecker->unwhitelist(ip)
├─ Store in cache/DB (permanent/temporary)
├─ recordBlock() → increment block count
└─ NawasaraService::block() (if enabled)
```
## Arsitektur File
```
packages/bale-gupa/
├── config/gupa.php # Semua konfigurasi + env vars
├── database/migrations/ # 4 migrasi (UUID PK)
│ ├── 2026_07_17_000001_create_gupa_blocked_ips_table.php
│ ├── 2026_07_17_000002_create_gupa_logs_table.php
│ ├── 2026_07_17_000003_create_gupa_whitelists_table.php
│ └── 2026_07_17_000004_create_gupa_blacklists_table.php
├── resources/views/components/ # Blade honeypot component
├── src/
│ ├── Actions/
│ │ ├── BlockAction.php # Block/unblock/pending/block count + Nawasara sync
│ │ ├── LogAction.php # Laravel log + DB log (database mode)
│ │ └── NotifyAction.php # Webhook + email notifications
│ ├── Commands/ # 9 Artisan commands
│ │ ├── AboutCommand.php # Config summary (nilai + sumber .env/environment/config)
│ │ ├── BlacklistCommand.php # --add/--remove/--list + Nawasara sync
│ │ ├── ClearScoreCommand.php # Reset score cache
│ │ ├── DashboardCommand.php # Real-time overview
│ │ ├── LogCommand.php # Query/prune logs
│ │ ├── SetupCommand.php # Interactive setup wizard
│ │ ├── StatsCommand.php # Config summary
│ │ ├── UnblockCommand.php # Unblock + Nawasara sync
│ │ └── WhitelistCommand.php # --add/--remove/--list
│ ├── Detectors/
│ │ ├── DetectorInterface.php # detect(), isEnabled(), getName()
│ │ ├── HeaderDetector.php # Bot UA, missing Accept/Accept-Language/Referer
│ │ ├── HoneypotDetector.php # Hidden field + route/prefix per-segment matching
│ │ ├── NotFoundDetector.php # Excessive 404 scanning
│ │ ├── RateLimitDetector.php# Laravel RateLimiter integration
│ │ └── VelocityDetector.php # Request rate per window
│ ├── Middleware/
│ │ └── GuardianMiddleware.php # Entry point — handle() + terminate()
│ ├── Models/
│ │ ├── Blacklist.php # gupa_blacklists (UUID)
│ │ ├── BlockedIp.php # gupa_blocked_ips (UUID, notExpired() scope)
│ │ ├── Log.php # gupa_logs (UUID, JSON metadata)
│ │ └── Whitelist.php # gupa_whitelists (UUID)
│ ├── Notifications/
│ │ ├── BlockNotification.php # DTO for notification
│ │ └── Channels/
│ │ ├── EmailChannel.php
│ │ └── WebhookChannel.php
│ ├── Scorer/
│ │ └── ScoreCalculator.php # Orchestrator — register, calculate, increment
│ └── Support/
│ ├── NawasaraService.php # HTTP client for Nawasara Security Center
│ └── WhitelistChecker.php # Whitelist/blacklist match (exact, CIDR, wildcard)
└── tests/ # Pest PHP tests
├── Feature/
│ └── GuardianMiddlewareTest.php
├── Unit/
│ ├── BlockActionTest.php
│ ├── CommandTest.php
│ ├── DatabaseModeTest.php
│ ├── HeaderDetectorTest.php
│ ├── HoneypotDetectorTest.php
│ ├── NawasaraServiceTest.php
│ ├── NotFoundDetectorTest.php
│ ├── NotifyActionTest.php
│ ├── RateLimitDetectorTest.php
│ ├── ScoreCalculatorTest.php
│ ├── SuspiciousPathTest.php
│ ├── VelocityDetectorTest.php
│ └── WhitelistCheckerTest.php
├── Pest.php
└── TestCase.php
```
---
## Storage Modes
| Mode (`GUPA_STORAGE`) | Backend | Block/Log Data | Score Data | Notes |
|---|---|---|---|---|
| `cache` (default) | Redis/file via `Cache` facade | Cache with TTL | Cache with TTL (decay window) | Hilang saat cache flush |
| `database` | SQLite/MySQL/Postgres | **Hanya** DB — zero cache touch | Cache tetap dipakai | Persistent, bisa query |
### Important: Pure DB Mode
When `GUPA_STORAGE=database`:
- `BlockAction::` all methods **skip ALL `Cache::` calls** — `isBlocked()`, `execute()`, `unblock()`, `setPendingBlock()`, `hasPendingBlock()`, `getBlockCount()` → direct DB queries via Eloquent
- `WhitelistChecker::` `whitelist()`, `unwhitelist()`, `blacklist()`, `unblacklist()`, `getWhitelistIps()`, `getBlacklistIps()` → direct DB queries
- Exception: `ScoreCalculator::increment()` **always uses cache** — scores are ephemeral
## Config (`config/gupa.php`)
Semua env vars:
```env
# Master
GUPA_ENABLED=true
GUPA_THRESHOLD=100
GUPA_SCORE_DECAY_WINDOW=300
GUPA_BLOCK_DURATION=3600
GUPA_LOG_ENABLED=true
GUPA_STORAGE=cache
GUPA_DB_CONNECTION= # nama koneksi dari config/database.php; kosong = koneksi default host
GUPA_SUSPICIOUS_THRESHOLD=10
GUPA_LOG_RETENTION_DAYS=30
GUPA_RECIDIVIST_DAYS=1
GUPA_RECIDIVIST_THRESHOLD=3
# Scoring (anti false-positive)
GUPA_SCORING_SKIP_ASSETS=true
GUPA_SCORING_ASSET_EXTENSIONS=css,js,mjs,map,png,jpg,jpeg,gif,svg,webp,avif,ico,woff,woff2,ttf,otf,eot,txt,xml
# Whitelist/Blacklist
GUPA_WHITELIST_ENABLED=true
GUPA_BLACKLIST_ENABLED=false
# Detectors
GUPA_VELOCITY_ENABLED=true
GUPA_HONEYPOT_ENABLED=true
GUPA_HEADER_ENABLED=true
GUPA_NOTFOUND_ENABLED=true
GUPA_RATE_LIMIT_DETECTOR_ENABLED=true
# Rate Limits
GUPA_RATE_LIMITS_ENABLED=false
# Notifications
GUPA_NOTIFICATIONS_ENABLED=false
GUPA_WEBHOOK_ENABLED=false
GUPA_EMAIL_ENABLED=false
# Nawasara (opsional)
GUPA_NAWASARA_ENABLED=false
GUPA_NAWASARA_BASE_URL=https://nawasara.ponorogo.go.id/api/v1/secscan
GUPA_NAWASARA_TOKEN=
GUPA_NAWASARA_TIMEOUT=10
```
---
## Koneksi & Model Custom
Default: **koneksi default host** + **model bawaan** `Bale\Gupa\Models\{BlockedIp, Log, Whitelist, Blacklist}`.
- `config('gupa.connection')` (env `GUPA_DB_CONNECTION`) — nama koneksi `config/database.php`. Bila di-set, dipakai oleh **query Gupa DAN migrasi** (`Schema::connection(...)`) serta `SetupCommand` (`DB::connection(...)`). Bila `null` → koneksi default host.
- `config('gupa.models.*')` — kelas model (`blocked_ip`, `log`, `whitelist`, `blacklist`). Default ke model bawaan. Ganti ke model custom (schema sama) untuk tabel/koneksi lain. Tidak perlu extend model Gupa.
**Prioritas koneksi:**
| Kondisi | Model bawaan | Model custom |
|---|---|---|
| `GUPA_DB_CONNECTION` di-set | pakai koneksi tsb | pakai koneksi tsb (override) |
| `GUPA_DB_CONNECTION` kosong | koneksi default host | `$connection` milik model (fallback default host) |
**Implementasi:** `Bale\Gupa\Support\ModelResolver`
- `blockedIp()/log()/whitelist()/blacklist(): class-string`
- `blockedIpQuery()/logQuery()/whitelistQuery()/blacklistQuery(): Builder` — builder sudah terhubung ke `gupa.connection` (bila di-set) atau `$class::query()` (bila tidak, agar `$connection` model custom tidak tertimpa).
Semua pemakaian model di `BlockAction`, `LogAction`, `WhitelistChecker`, `DashboardCommand`, `LogCommand` lewat `ModelResolver` — **jangan** hardcode `Bale\Gupa\Models\*` di luar resolver & konfigurasi.
---
## Detectors
| Class | File | Score Default | What It Catches |
|---|---|---|---|
| `VelocityDetector` | `Detectors/VelocityDetector.php` | 15 | Request rate > 60 per 60 detik |
| `HoneypotDetector` | `Detectors/HoneypotDetector.php` | 50 | Hidden field filled, route/prefix match (per-segment) |
| `HeaderDetector` | `Detectors/HeaderDetector.php` | 20+10+10+5 | Bot UA (16 hardcoded), missing Accept, missing Accept-Language, missing Referer on POST |
| `NotFoundDetector` | `Detectors/NotFoundDetector.php` | 20 | >10 requests getting 404 in 60s |
| `RateLimitDetector` | `Detectors/RateLimitDetector.php` | 10 | Exceeded Laravel rate limiter |
Each implements `DetectorInterface`:
- `detect(Request $request): int`
- `isEnabled(): bool`
- `getName(): string`
### HeaderDetector: 16 Hardcoded Bot User-Agents
`scrapy`, `curl`, `wget`, `python-requests`, `python-urllib`, `go-http-client`, `php/`, `java/`, `perl`, `ruby`, `nikto`, `sqlmap`, `masscan`, `nmap`, `zgrab`, `httpx`
### HoneypotDetector: Per-Segment Matching
Path di-split by `/`, setiap segment di-match ke `routes` (exact) dan `prefixes` (awalan).
### NotFoundDetector
Menggunakan `Cache` untuk tracking 404 count per IP dengan window.
---
## Penting: Whitelist/Blacklist CIDR & Wildcard
`WhitelistChecker::matchIp()` support 3 format:
1. **Exact**: `192.168.1.1`
2. **CIDR**: `192.168.1.0/24`, `10.0.0.0/8`
3. **Wildcard**: `192.168.*.*`, `10.0.1.*`
### Mutual Exclusion
- Adding to blacklist **auto-removes** from whitelist
- Adding to whitelist **auto-removes** from blacklist
### Config vs Dynamic
- Config: `config/gupa.php` `whitelist.ips[]` / `blacklist.ips[]`
- Dynamic: `php artisan gupa:blacklist --add=1.2.3.4` (stored in cache or DB)
---
## Recidivist System
Configurable via env:
- `GUPA_RECIDIVIST_DAYS` (default: 1) — window in days
- `GUPA_RECIDIVIST_THRESHOLD` (default: 3) — count before permanent block
Logic di `GuardianMiddleware::executeBlock()`:
```php
$blockCount = $this->blockAction->getBlockCount($ip);
$isRecidivist = $blockCount >= $maxBlocks; // GUPA_RECIDIVIST_THRESHOLD
if ($isRecidivist) {
// permanent block + LogAction::permanentBlock()
} else {
// temporary block + LogAction::block()
}
```
### Block Count Storage
- **Cache mode**: `Cache::put('gupa:block_count:{ip}', $count, $recidivistDays * 86400)`
- **DB mode**: `gupa_logs` table, count `block`/`permanent_block` events within `recidivist_days`
---
## Pending Block System
Mechanism untuk defer block ke request berikutnya:
1. `NotFoundDetector` deteksi 404 → panggil `BlockAction::setPendingBlock()`
2. Request berikutnya dari IP sama → `GuardianMiddleware::handle()` deteksi `hasPendingBlock()` → `applyPendingBlock()` → `execute()` → 403
Pending blocks disimpan di:
- **Cache mode**: `Cache::put('gupa:pending_block:{ip}', true, $duration)`
- **DB mode**: `gupa_blocked_ips` dengan `reason='pending'` (excluded from `isBlocked()` check)
---
## Nawasara Integration
Opsional. Aktif via `GUPA_NAWASARA_ENABLED=true` + valid token.
| Endpoint | Method | Trigger |
|---|---|---|
| `/ip-blocks` | POST | `BlockAction::execute()` + `BlacklistCommand --add` |
| `/ip-blocks/{ip}` | DELETE | `BlockAction::unblock()` + `BlacklistCommand --remove` |
| `/ip-blocks` | GET | N/A (read-only) |
Sync via `NawasaraService` (singleton, registered di `GupaServiceProvider`):
- `block(ip, reason): bool`
- `unblock(ip): bool`
- `list(): array`
- `isEnabled(): bool`
---
## Important Gotchas
### PHP / Namespace
- **No `HasUuid` trait** — semua model manual `Str::uuid()` di `creating` event
- **BlacklistCommand** — `handle()` punya private methods inlined untuk menghindari PHP namespace resolution issue dengan `use` imports di method signatures
### Log Model
- `created_at` **tidak** di `$fillable` — test perlu `forceCreate()` untuk setting past dates
- `created_at` kolom di migration pakai `useCurrent()` default
### Setup Command
- `gupa:setup` Advance mode cek DB connection **sebelum** migrations
- Graceful skip jika koneksi gagal
### GuardianMiddleware
- `handle()` — rate limit check **sebelum** response dibuat
- `terminate()` — scoring **setelah** response dikirim ke client
- `terminate()` — **skip scoring untuk aset statis** (`.css/.js/.png/...` via `gupa.scoring.skip_assets`) untuk cegah false positive; velocity hanya berskor **sekali per window** (`gupa:velocity:scored:{ip}` flag)
- `TrustProxies::at('*')` harus di `bootstrap/app.php` untuk cloudflared + traefik proxy IP resolution
### BlockAction
- `isBlockedInDatabase()` excludes `reason='pending'` rows — pending blocks **tidak** dianggap blocked
- `execute()` calls `whitelistChecker->unwhitelist()` untuk auto-remove dari whitelist
- `recordBlock()` di cache mode — saat `GUPA_STORAGE=database`, method ini **return early** (block count dihitung via query `gupa_logs`)
### WhitelistChecker
- `getWhitelistIps()` dan `getBlacklistIps()` — **merge** config + dynamic entries
- `syncFromDatabase()` — untuk cache mode, sync DB → cache (manual call)
- `matchesCidr()` — public, bisa dipanggil langsung
### ScoreCalculator
- **Always** menggunakan cache — score bersifat ephemeral dengan `score_decay_window`
- Tidak terpengaruh `GUPA_STORAGE`
---
## Commands
```bash
# Monitoring
php artisan gupa:about # Ringkasan konfigurasi (nilai + sumber .env/environment/default)
php artisan gupa:dashboard # Real-time overview
php artisan gupa:dashboard --json # JSON output
php artisan gupa:stats # Config summary
php artisan gupa:log # Recent logs (DB mode)
php artisan gupa:log --prune # Hapus log expired
# IP Management
php artisan gupa:unblock 1.2.3.4 # Unblock + Nawasara sync
php artisan gupa:clear-score 1.2.3.4 # Reset score only
# Whitelist/Blacklist
php artisan gupa:whitelist --add=10.0.0.0/8
php artisan gupa:whitelist --remove=1.2.3.4
php artisan gupa:whitelist --list
php artisan gupa:blacklist --add=1.2.3.4
php artisan gupa:blacklist --remove=1.2.3.4
php artisan gupa:blacklist --list
# Setup
php artisan gupa:setup # Interactive wizard
```
---
## Testing
```bash
cd packages/bale-gupa
composer test # semua tests
composer test-coverage # dengan coverage
composer format # Laravel Pint
```
### Test Patterns
- **Pest PHP** — semua test menggunakan Pest
- **Base TestCase** extends `Orchestra\Testbench\TestCase`
- **Config setup** di `defineEnvironment()` via `config()->set()`
- **Migrations** loading via `loadMigrationsFrom()`
- **Route** `/test-guardian` registered with `gupa` middleware
- **Storage default** di test: `cache` mode
- **Database test** menggunakan SQLite `:memory:`
### Test Coverage Areas
- Unit: setiap detector, action, support class
- Feature: GuardianMiddleware integration (block, whitelist, blacklist, pending block, rate limit)
- DatabaseMode: pure DB mode (30 assertions)
- NawasaraService: 11 tests (enabled/disabled, block/unblock/list, error handling, bearer token)
### Writing Tests for CIDR/IP Matching
```php
$checker = app(WhitelistChecker::class);
expect($checker->matchesCidr('192.168.1.10', '192.168.1.0/24'))->toBeTrue();
expect($checker->matchesWildcard('192.168.1.1', '192.168.*.*'))->toBeTrue();
```
---
## Security Considerations
- **User-agent is fully client-controlled** — jangan pernah trust UA alone untuk allowlist
- **WhatsApp prefetch false positive** — prefetch kirim ~3 request/share, missing `Accept` (+10) dan `Accept-Language` (+10) → scores accumulate 10→30→50→60→80→100 → BLOCKED
- **CIDR + Wildcard support** sudah built-in di `WhitelistChecker`
- **Pending blocks** — excluded dari blocked check via `reason='pending'` di DB
## Development
```bash
# Install dependencies
composer install
# Run tests
composer test
# Format code
composer format
```
Package:
bale/gupa| Namespace:Bale\Gupa| Brand: Bale ecosystemPerlindungan IP, rate limiting, dan deteksi bot untuk aplikasi Laravel 11/12/13.
Gupa adalah lapisan keamanan komprehensif yang memberi skor setiap request secara real-time via multi-detektor, memblokir IP jahat (sementara/permanen), dan menyediakan CLI untuk monitoring/manajemen.
Request → GuardianMiddleware::handle()
│
├─ Disabled? → next
├─ Whitelisted? → next
├─ Blacklisted? → 403
├─ Already blocked? → 403
├─ Pending block? → apply → 403
├─ Rate limited? → 429
│
└─ next(request) → response
│
└─ GuardianMiddleware::terminate()
│
├─ Whitelisted/Blocked? → skip
├─ 404? → recordNotFound()
└─ processScoring()
│
├─ ScoreCalculator::calculate()
│ └─ iterates 5 detectors → sum score
│
├─ ScoreCalculator::increment() → cache
│
├─ LogAction::logSuspiciousRequest()
│
└─ If score >= threshold → BlockAction::execute()
├─ whitelistChecker->unwhitelist(ip)
├─ Store in cache/DB (permanent/temporary)
├─ recordBlock() → increment block count
└─ NawasaraService::block() (if enabled)
packages/bale-gupa/
├── config/gupa.php # Semua konfigurasi + env vars
├── database/migrations/ # 4 migrasi (UUID PK)
│ ├── 2026_07_17_000001_create_gupa_blocked_ips_table.php
│ ├── 2026_07_17_000002_create_gupa_logs_table.php
│ ├── 2026_07_17_000003_create_gupa_whitelists_table.php
│ └── 2026_07_17_000004_create_gupa_blacklists_table.php
├── resources/views/components/ # Blade honeypot component
├── src/
│ ├── Actions/
│ │ ├── BlockAction.php # Block/unblock/pending/block count + Nawasara sync
│ │ ├── LogAction.php # Laravel log + DB log (database mode)
│ │ └── NotifyAction.php # Webhook + email notifications
│ ├── Commands/ # 9 Artisan commands
│ │ ├── AboutCommand.php # Config summary (nilai + sumber .env/environment/config)
│ │ ├── BlacklistCommand.php # --add/--remove/--list + Nawasara sync
│ │ ├── ClearScoreCommand.php # Reset score cache
│ │ ├── DashboardCommand.php # Real-time overview
│ │ ├── LogCommand.php # Query/prune logs
│ │ ├── SetupCommand.php # Interactive setup wizard
│ │ ├── StatsCommand.php # Config summary
│ │ ├── UnblockCommand.php # Unblock + Nawasara sync
│ │ └── WhitelistCommand.php # --add/--remove/--list
│ ├── Detectors/
│ │ ├── DetectorInterface.php # detect(), isEnabled(), getName()
│ │ ├── HeaderDetector.php # Bot UA, missing Accept/Accept-Language/Referer
│ │ ├── HoneypotDetector.php # Hidden field + route/prefix per-segment matching
│ │ ├── NotFoundDetector.php # Excessive 404 scanning
│ │ ├── RateLimitDetector.php# Laravel RateLimiter integration
│ │ └── VelocityDetector.php # Request rate per window
│ ├── Middleware/
│ │ └── GuardianMiddleware.php # Entry point — handle() + terminate()
│ ├── Models/
│ │ ├── Blacklist.php # gupa_blacklists (UUID)
│ │ ├── BlockedIp.php # gupa_blocked_ips (UUID, notExpired() scope)
│ │ ├── Log.php # gupa_logs (UUID, JSON metadata)
│ │ └── Whitelist.php # gupa_whitelists (UUID)
│ ├── Notifications/
│ │ ├── BlockNotification.php # DTO for notification
│ │ └── Channels/
│ │ ├── EmailChannel.php
│ │ └── WebhookChannel.php
│ ├── Scorer/
│ │ └── ScoreCalculator.php # Orchestrator — register, calculate, increment
│ └── Support/
│ ├── NawasaraService.php # HTTP client for Nawasara Security Center
│ └── WhitelistChecker.php # Whitelist/blacklist match (exact, CIDR, wildcard)
└── tests/ # Pest PHP tests
├── Feature/
│ └── GuardianMiddlewareTest.php
├── Unit/
│ ├── BlockActionTest.php
│ ├── CommandTest.php
│ ├── DatabaseModeTest.php
│ ├── HeaderDetectorTest.php
│ ├── HoneypotDetectorTest.php
│ ├── NawasaraServiceTest.php
│ ├── NotFoundDetectorTest.php
│ ├── NotifyActionTest.php
│ ├── RateLimitDetectorTest.php
│ ├── ScoreCalculatorTest.php
│ ├── SuspiciousPathTest.php
│ ├── VelocityDetectorTest.php
│ └── WhitelistCheckerTest.php
├── Pest.php
└── TestCase.php
Mode (GUPA_STORAGE) |
Backend | Block/Log Data | Score Data | Notes |
|---|---|---|---|---|
cache (default) |
Redis/file via Cache facade |
Cache with TTL | Cache with TTL (decay window) | Hilang saat cache flush |
database |
SQLite/MySQL/Postgres | Hanya DB — zero cache touch | Cache tetap dipakai | Persistent, bisa query |
When GUPA_STORAGE=database:
BlockAction:: all methods skip ALL Cache:: calls — isBlocked(), execute(), unblock(), setPendingBlock(), hasPendingBlock(), getBlockCount() → direct DB queries via EloquentWhitelistChecker:: whitelist(), unwhitelist(), blacklist(), unblacklist(), getWhitelistIps(), getBlacklistIps() → direct DB queriesScoreCalculator::increment() always uses cache — scores are ephemeralconfig/gupa.php)Semua env vars:
# Master
GUPA_ENABLED=true
GUPA_THRESHOLD=100
GUPA_SCORE_DECAY_WINDOW=300
GUPA_BLOCK_DURATION=3600
GUPA_LOG_ENABLED=true
GUPA_STORAGE=cache
GUPA_DB_CONNECTION= # nama koneksi dari config/database.php; kosong = koneksi default host
GUPA_SUSPICIOUS_THRESHOLD=10
GUPA_LOG_RETENTION_DAYS=30
GUPA_RECIDIVIST_DAYS=1
GUPA_RECIDIVIST_THRESHOLD=3
# Scoring (anti false-positive)
GUPA_SCORING_SKIP_ASSETS=true
GUPA_SCORING_ASSET_EXTENSIONS=css,js,mjs,map,png,jpg,jpeg,gif,svg,webp,avif,ico,woff,woff2,ttf,otf,eot,txt,xml
# Whitelist/Blacklist
GUPA_WHITELIST_ENABLED=true
GUPA_BLACKLIST_ENABLED=false
# Detectors
GUPA_VELOCITY_ENABLED=true
GUPA_HONEYPOT_ENABLED=true
GUPA_HEADER_ENABLED=true
GUPA_NOTFOUND_ENABLED=true
GUPA_RATE_LIMIT_DETECTOR_ENABLED=true
# Rate Limits
GUPA_RATE_LIMITS_ENABLED=false
# Notifications
GUPA_NOTIFICATIONS_ENABLED=false
GUPA_WEBHOOK_ENABLED=false
GUPA_EMAIL_ENABLED=false
# Nawasara (opsional)
GUPA_NAWASARA_ENABLED=false
GUPA_NAWASARA_BASE_URL=https://nawasara.ponorogo.go.id/api/v1/secscan
GUPA_NAWASARA_TOKEN=
GUPA_NAWASARA_TIMEOUT=10
Default: koneksi default host + model bawaan Bale\Gupa\Models\{BlockedIp, Log, Whitelist, Blacklist}.
config('gupa.connection') (env GUPA_DB_CONNECTION) — nama koneksi config/database.php. Bila di-set, dipakai oleh query Gupa DAN migrasi (Schema::connection(...)) serta SetupCommand (DB::connection(...)). Bila null → koneksi default host.config('gupa.models.*') — kelas model (blocked_ip, log, whitelist, blacklist). Default ke model bawaan. Ganti ke model custom (schema sama) untuk tabel/koneksi lain. Tidak perlu extend model Gupa.Prioritas koneksi:
| Kondisi | Model bawaan | Model custom |
|---|---|---|
GUPA_DB_CONNECTION di-set |
pakai koneksi tsb | pakai koneksi tsb (override) |
GUPA_DB_CONNECTION kosong |
koneksi default host | $connection milik model (fallback default host) |
Implementasi: Bale\Gupa\Support\ModelResolver
blockedIp()/log()/whitelist()/blacklist(): class-stringblockedIpQuery()/logQuery()/whitelistQuery()/blacklistQuery(): Builder — builder sudah terhubung ke gupa.connection (bila di-set) atau $class::query() (bila tidak, agar $connection model custom tidak tertimpa).Semua pemakaian model di BlockAction, LogAction, WhitelistChecker, DashboardCommand, LogCommand lewat ModelResolver — jangan hardcode Bale\Gupa\Models\* di luar resolver & konfigurasi.
| Class | File | Score Default | What It Catches |
|---|---|---|---|
VelocityDetector |
Detectors/VelocityDetector.php |
15 | Request rate > 60 per 60 detik |
HoneypotDetector |
Detectors/HoneypotDetector.php |
50 | Hidden field filled, route/prefix match (per-segment) |
HeaderDetector |
Detectors/HeaderDetector.php |
20+10+10+5 | Bot UA (16 hardcoded), missing Accept, missing Accept-Language, missing Referer on POST |
NotFoundDetector |
Detectors/NotFoundDetector.php |
20 | >10 requests getting 404 in 60s |
RateLimitDetector |
Detectors/RateLimitDetector.php |
10 | Exceeded Laravel rate limiter |
Each implements DetectorInterface:
detect(Request $request): intisEnabled(): boolgetName(): stringscrapy, curl, wget, python-requests, python-urllib, go-http-client, php/, java/, perl, ruby, nikto, sqlmap, masscan, nmap, zgrab, httpx
Path di-split by /, setiap segment di-match ke routes (exact) dan prefixes (awalan).
Menggunakan Cache untuk tracking 404 count per IP dengan window.
WhitelistChecker::matchIp() support 3 format:
192.168.1.1192.168.1.0/24, 10.0.0.0/8192.168.*.*, 10.0.1.*config/gupa.php whitelist.ips[] / blacklist.ips[]php artisan gupa:blacklist --add=1.2.3.4 (stored in cache or DB)Configurable via env:
GUPA_RECIDIVIST_DAYS (default: 1) — window in daysGUPA_RECIDIVIST_THRESHOLD (default: 3) — count before permanent blockLogic di GuardianMiddleware::executeBlock():
$blockCount = $this->blockAction->getBlockCount($ip);
$isRecidivist = $blockCount >= $maxBlocks; // GUPA_RECIDIVIST_THRESHOLD
if ($isRecidivist) {
// permanent block + LogAction::permanentBlock()
} else {
// temporary block + LogAction::block()
}
Cache::put('gupa:block_count:{ip}', $count, $recidivistDays * 86400)gupa_logs table, count block/permanent_block events within recidivist_daysMechanism untuk defer block ke request berikutnya:
NotFoundDetector deteksi 404 → panggil BlockAction::setPendingBlock()GuardianMiddleware::handle() deteksi hasPendingBlock() → applyPendingBlock() → execute() → 403Pending blocks disimpan di:
Cache::put('gupa:pending_block:{ip}', true, $duration)gupa_blocked_ips dengan reason='pending' (excluded from isBlocked() check)Opsional. Aktif via GUPA_NAWASARA_ENABLED=true + valid token.
| Endpoint | Method | Trigger |
|---|---|---|
/ip-blocks |
POST | BlockAction::execute() + BlacklistCommand --add |
/ip-blocks/{ip} |
DELETE | BlockAction::unblock() + BlacklistCommand --remove |
/ip-blocks |
GET | N/A (read-only) |
Sync via NawasaraService (singleton, registered di GupaServiceProvider):
block(ip, reason): boolunblock(ip): boollist(): arrayisEnabled(): boolHasUuid trait — semua model manual Str::uuid() di creating eventhandle() punya private methods inlined untuk menghindari PHP namespace resolution issue dengan use imports di method signaturescreated_at tidak di $fillable — test perlu forceCreate() untuk setting past datescreated_at kolom di migration pakai useCurrent() defaultgupa:setup Advance mode cek DB connection sebelum migrationshandle() — rate limit check sebelum response dibuatterminate() — scoring setelah response dikirim ke clientterminate() — skip scoring untuk aset statis (.css/.js/.png/... via gupa.scoring.skip_assets) untuk cegah false positive; velocity hanya berskor sekali per window (gupa:velocity:scored:{ip} flag)TrustProxies::at('*') harus di bootstrap/app.php untuk cloudflared + traefik proxy IP resolutionisBlockedInDatabase() excludes reason='pending' rows — pending blocks tidak dianggap blockedexecute() calls whitelistChecker->unwhitelist() untuk auto-remove dari whitelistrecordBlock() di cache mode — saat GUPA_STORAGE=database, method ini return early (block count dihitung via query gupa_logs)getWhitelistIps() dan getBlacklistIps() — merge config + dynamic entriessyncFromDatabase() — untuk cache mode, sync DB → cache (manual call)matchesCidr() — public, bisa dipanggil langsungscore_decay_windowGUPA_STORAGE# Monitoring
php artisan gupa:about # Ringkasan konfigurasi (nilai + sumber .env/environment/default)
php artisan gupa:dashboard # Real-time overview
php artisan gupa:dashboard --json # JSON output
php artisan gupa:stats # Config summary
php artisan gupa:log # Recent logs (DB mode)
php artisan gupa:log --prune # Hapus log expired
# IP Management
php artisan gupa:unblock 1.2.3.4 # Unblock + Nawasara sync
php artisan gupa:clear-score 1.2.3.4 # Reset score only
# Whitelist/Blacklist
php artisan gupa:whitelist --add=10.0.0.0/8
php artisan gupa:whitelist --remove=1.2.3.4
php artisan gupa:whitelist --list
php artisan gupa:blacklist --add=1.2.3.4
php artisan gupa:blacklist --remove=1.2.3.4
php artisan gupa:blacklist --list
# Setup
php artisan gupa:setup # Interactive wizard
cd packages/bale-gupa
composer test # semua tests
composer test-coverage # dengan coverage
composer format # Laravel Pint
Orchestra\Testbench\TestCasedefineEnvironment() via config()->set()loadMigrationsFrom()/test-guardian registered with gupa middlewarecache mode:memory:$checker = app(WhitelistChecker::class);
expect($checker->matchesCidr('192.168.1.10', '192.168.1.0/24'))->toBeTrue();
expect($checker->matchesWildcard('192.168.1.1', '192.168.*.*'))->toBeTrue();
Accept (+10) dan Accept-Language (+10) → scores accumulate 10→30→50→60→80→100 → BLOCKEDWhitelistCheckerreason='pending' di DB# Install dependencies
composer install
# Run tests
composer test
# Format code
composer format
How can I help you explore Laravel packages today?