Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
Agents
bale/gupa
claude-code
cursor
security
laravel
guardian
Bale
bot-detection

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.

Install
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
```

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:

# 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():

$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

# 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

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

$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

# Install dependencies
composer install

# Run tests
composer test

# Format code
composer format
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core