pasax-secure-monitorPanduan Integrasi·5 min baca

Panduan Kustomisasi Penuh (Full Custom Suite: Blade & REST API) Tanpa Mengubah Vendor

Paket pasax/secure-monitor dirancang mengikuti filosofi Spatie-Standard & Headless Architecture. Artinya, setelah paket dipasang melalui composer require...

Paket pasax/secure-monitor dirancang mengikuti filosofi Spatie-Standard & Headless Architecture. Artinya, setelah paket dipasang melalui composer require pasax/secure-monitor, Anda memiliki kebebasan dan kendali 100% penuh untuk membuat tampilan monitoring kustom, alur kerja keamanan sendiri, maupun antarmuka REST API JSON tanpa pernah menyentuh atau merusak berkas inti paket di vendor/.

Dokumen ini adalah cetak biru (blueprint) dan implementasi referensi resmi percontohan menyeluruh bagi pengembang yang ingin membangun dashboard Blade dan REST API kustom mencakup seluruh 8 modul keamanan Pasax.


1. Aturan Emas: Kebijakan Dilarang Mengubah Vendor (*Zero-Vendor-Edit Policy*)#

⚠️ PERINGATAN KRUSIAL: JANGAN PERNAH memodifikasi, mengedit, atau menghapus berkas apapun di dalam folder vendor/pasax/secure-monitor/. Segala perubahan di dalam folder vendor/ bersifat sementara dan berbahaya!

Mengapa Dilarang Mengubah Folder `vendor/`?#

  1. Pembaruan Composer Menimpa Perubahan: Setiap kali Anda menjalankan composer update atau men-deploy proyek ke server produksi via CI/CD, Composer akan menghapus dan mengunduh ulang folder vendor/. Seluruh modifikasi kustom yang Anda tulis di sana akan hilang permanen.
  2. Integritas Deteksi & ReDoS Immunity: Berkas regex WAF di src/Services/PasaxService.php telah dirancang secara matematis untuk kebal terhadap serangan Regular Expression Denial of Service (ReDoS). Mengubah pola regex langsung di vendor dapat membuat server Anda lumpuh akibat catastrophic backtracking.
  3. Integritas Signature & Baseline: Modul server audit Pasax memeriksa SHA-256 dan integritas pustaka. Modifikasi internal akan memicu peringatan manipulasi berkas sistem.

2. Memahami Batas Wilayah: Apa yang Ada di `vendor/` vs Aplikasi Anda#

Ketika Anda menjalankan:

composer require pasax/secure-monitor
php artisan pasax:install

Berikut adalah pemisahan peran yang tegas antara pustaka inti dan aplikasi Anda:

PROYEK LARAVEL ANDA
├── app/
│   └── Http/Controllers/
│       ├── KeamananController.php          <-- [ZONA ANDA] Controller Blade kustom (8 Modul)
│       └── Api/KeamananController.php      <-- [ZONA ANDA] REST API JSON kustom (8 Modul)
├── config/
│   └── pasax.php                           <-- [ZONA ANDA] Konfigurasi aman diubah
├── resources/views/
│   ├── layouts/app.blade.php               <-- [ZONA ANDA] Template induk aplikasi Anda
│   ├── keamanan/                           <-- [ZONA ANDA] Suite Tampilan Kustom Penuh:
│   │   ├── _nav.blade.php                      (Komponen tab navigasi bersama)
│   │   ├── index.blade.php                     (1. Ringkasan & Kartu Statistik)
│   │   ├── logs.blade.php                      (2. Riwayat Log Ancaman & Filter)
│   │   ├── blocked-ips.blade.php               (3. Isolasi IP & Device Quarantine)
│   │   ├── allowed-ips.blade.php               (4. Whitelist IP & Jaringan CIDR)
│   │   ├── server.blade.php                    (5. Audit Server & Integritas SHA-256)
│   │   ├── sessions.blade.php                  (6. Sesi Pengguna & IP Terpercaya)
│   │   ├── tickets.blade.php                   (7. Tiket Banding Permohonan Unblock)
│   │   └── settings.blade.php                  (8. Pengaturan WAF Runtime Dinamis)
├── routes/
│   ├── web.php                             <-- [ZONA ANDA] Rute web kustom (/keamanan/*)
│   ├── api.php                             <-- [ZONA ANDA] Rute API kustom (/api/keamanan/*)
│   ├── pasax.php                           <-- [ZONA ANDA] Rute bawaan paket (bisa disesuaikan)
│   └── pasax-service.php                       <-- [ZONA ANDA] Rute API bawaan paket
│
└── vendor/pasax/secure-monitor/                   <-- [ZONA DILINDUNGI / READ-ONLY]
    ├── src/                                    (Engine WAF, Service, Middleware, Model)
    │   ├── Facades/Pasax.php                   (Facade resmi untuk dipanggil aplikasi Anda)
    │   ├── Services/PasaxService.php           (Mesin deteksi ancaman & inspeksi payload)
    │   ├── Services/PasaxServerSecurityService.php (Engine audit server & baseline SHA-256)
    │   ├── Http/Middleware/                    (Middleware pemblokir & penganalisis ancaman)
    │   └── Models/                             (Model Eloquent: PasaxLog, PasaxBlockedIp, dll.)
    ├── database/migrations/                    (Definisi skema tabel migrasi)
    └── stubs/                                  (Template publikasi)
  • vendor/pasax/secure-monitor (Zona Mesin / Engine): Layak dilihat hanya untuk referensi audit (read-only). Berisi logika inti WAF, parser header reverse-proxy, dan filter keamanan tingkat rendah.
  • Aplikasi Host Anda (Zona Pengembang): Semua penyesuaian tampilan, rute, otorisasi, dan integrasi UI dilakukan di sini.

3. Komponen Inti yang Disediakan Paket untuk Kode Kustom Anda#

Aplikasi Anda cukup memanggil komponen resmi berikut:

Komponen Namespace / FQCN Peran & Fungsi
Facade Pasax Internal\Pasax\Facades\Pasax Akses statistik (stats()), cek blokir (isBlocked()), blokir IP (blockIp()), unblock (unblockIp()), runtime settings (getSettings(), updateSettings()).
Server Security Service Internal\Pasax\Services\PasaxServerSecurityService Audit konfigurasi PHP/DB/Storage (scan()), buat baseline SHA-256 (createBaseline()), hapus berkas mencurigakan defensif (deleteSuspiciousFile()).
Model Log Ancaman Internal\Pasax\Models\PasaxLog Query Eloquent ke riwayat ancaman WAF (threat_level, ip_address, path, payload).
Model IP Terblokir Internal\Pasax\Models\PasaxBlockedIp Manajemen IP & device terisolasi (ip_address, device_id, block_scope, is_active).
Model Whitelist Internal\Pasax\Models\PasaxAllowedIp Daftar pengecualian IP/CIDR yang kebal deteksi WAF.
Model Percobaan Login Internal\Pasax\Models\PasaxLoginAttempt Pelacakan mitigasi brute-force bertingkat (stepped throttling).
Model Sesi Pengguna Internal\Pasax\Models\PasaxUserLogin Riwayat sesi aktif pengguna, heartbeat, dan data perangkat login.
Model IP Terpercaya Internal\Pasax\Models\PasaxTrustedIp Alamat IP admin/staf yang diverifikasi sah.
Model Tiket Banding Internal\Pasax\Models\PasaxIpUnblockRequest Alur self-service appeal bagi pengguna yang terblokir tidak sengaja.

4. Implementasi Penuh Web Controller Host (`app/Http/Controllers/KeamananController.php`)#

Buat berkas kustom di dalam aplikasi host Anda:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Internal\Pasax\Facades\Pasax;
use Internal\Pasax\Models\PasaxAllowedIp;
use Internal\Pasax\Models\PasaxBlockedIp;
use Internal\Pasax\Models\PasaxIpUnblockRequest;
use Internal\Pasax\Models\PasaxLog;
use Internal\Pasax\Models\PasaxLoginAttempt;
use Internal\Pasax\Models\PasaxTrustedIp;
use Internal\Pasax\Models\PasaxUserLogin;
use Internal\Pasax\Services\PasaxServerSecurityService;

class KeamananController extends Controller
{
    // 1. OVERVIEW / RINGKASAN
    public function index(Request $request): View
    {
        return view('keamanan.index', [
            'stats' => Pasax::stats(),
            'levelBreakdown' => Pasax::levelBreakdown(24),
            'topAttackers' => Pasax::topAttackers(5),
            'recentLogs' => PasaxLog::query()->latest('id')->limit(8)->get(),
            'recentBlocks' => PasaxBlockedIp::query()->active()->latest('id')->limit(5)->get(),
            'wafEnabled' => Pasax::isEnabled(),
            'enforcementEnabled' => Pasax::isBlockEnforcementEnabled(),
        ]);
    }

    // 2. LOG ANCAMAN WAF
    public function logs(Request $request): View
    {
        $logs = PasaxLog::query()
            ->when($request->filled('level'), fn ($q) => $q->where('threat_level', $request->query('level')))
            ->when($request->filled('search'), function ($query) use ($request) {
                $search = $request->query('search');
                $query->where(fn ($sub) => $sub->where('ip_address', 'like', "%{$search}%")
                    ->orWhere('path', 'like', "%{$search}%")
                    ->orWhere('rule_label', 'like', "%{$search}%"));
            })
            ->latest('id')
            ->paginate(15)
            ->withQueryString();

        return view('keamanan.logs', [
            'logs' => $logs,
            'filterLevel' => $request->query('level', ''),
            'filterSearch' => $request->query('search', ''),
        ]);
    }

    public function destroyLog(int $id): RedirectResponse
    {
        PasaxLog::destroy($id);
        return back()->with('status', "Log ancaman #{$id} berhasil dihapus.");
    }

    public function clearLogs(): RedirectResponse
    {
        PasaxLog::truncate();
        return back()->with('status', 'Semua riwayat log ancaman WAF berhasil dibersihkan.');
    }

    // 3. IP DIBLOKIR & DEVICE QUARANTINE
    public function blockedIps(Request $request): View
    {
        $blockedIps = PasaxBlockedIp::query()
            ->when($request->filled('search'), function ($q) use ($request) {
                $search = $request->query('search');
                $q->where(fn ($sub) => $sub->where('ip_address', 'like', "%{$search}%")
                    ->orWhere('reason', 'like', "%{$search}%"));
            })
            ->latest('id')
            ->paginate(15)
            ->withQueryString();

        return view('keamanan.blocked-ips', [
            'blockedIps' => $blockedIps,
            'filterSearch' => $request->query('search', ''),
        ]);
    }

    public function blockIp(Request $request): RedirectResponse
    {
        $validated = $request->validate([
            'ip' => ['required', 'ip'],
            'reason' => ['nullable', 'string', 'max:255'],
            'hours' => ['nullable', 'integer', 'min:1', 'max:8760'],
            'scope' => ['nullable', 'string', 'in:ip,device'],
        ]);

        Pasax::blockIp(
            ip: $validated['ip'],
            reason: $validated['reason'] ?: 'Diblokir manual dari Dashboard Keamanan Kustom',
            durationHours: ! empty($validated['hours']) ? (int) $validated['hours'] : null,
            source: 'manual',
            userId: $request->user()?->id ? (int) $request->user()->id : null,
            scope: $validated['scope'] ?? 'ip'
        );

        return back()->with('status', "IP {$validated['ip']} berhasil diblokir.");
    }

    public function unblockIp(string $ip): RedirectResponse
    {
        Pasax::unblockIp($ip);
        return back()->with('status', "Blokir untuk IP {$ip} berhasil dibuka.");
    }

    public function toggleBlockedIp(int $id): RedirectResponse
    {
        $item = PasaxBlockedIp::findOrFail($id);
        $item->update(['is_active' => ! $item->is_active]);
        $status = $item->is_active ? 'diaktifkan kembali' : 'dinonaktifkan';

        return back()->with('status', "Status blokir untuk IP {$item->ip_address} berhasil {$status}.");
    }

    public function destroyBlockedIp(int $id): RedirectResponse
    {
        $item = PasaxBlockedIp::findOrFail($id);
        $ip = $item->ip_address;
        $item->delete();

        return back()->with('status', "Rekaman blokir IP {$ip} berhasil dihapus.");
    }

    // 4. WHITELIST IP (ALLOWED IPS)
    public function allowedIps(Request $request): View
    {
        $allowedIps = PasaxAllowedIp::query()->latest('id')->paginate(15);
        return view('keamanan.allowed-ips', ['allowedIps' => $allowedIps]);
    }

    public function storeAllowedIp(Request $request): RedirectResponse
    {
        $validated = $request->validate([
            'ip_address' => ['required', 'string', 'max:50'],
            'label' => ['nullable', 'string', 'max:100'],
            'notes' => ['nullable', 'string', 'max:255'],
        ]);

        PasaxAllowedIp::create([
            'ip_address' => $validated['ip_address'],
            'label' => $validated['label'] ?: 'Whitelist Manual',
            'notes' => $validated['notes'] ?? null,
            'is_active' => true,
            'allowed_by' => $request->user()?->id,
        ]);

        return back()->with('status', "IP / CIDR {$validated['ip_address']} berhasil ditambahkan ke whitelist.");
    }

    public function toggleAllowedIp(int $id): RedirectResponse
    {
        $item = PasaxAllowedIp::findOrFail($id);
        $item->update(['is_active' => ! $item->is_active]);
        $status = $item->is_active ? 'diaktifkan' : 'dinonaktifkan';

        return back()->with('status', "Whitelist {$item->ip_address} berhasil {$status}.");
    }

    public function destroyAllowedIp(int $id): RedirectResponse
    {
        $item = PasaxAllowedIp::findOrFail($id);
        $item->delete();
        return back()->with('status', 'Alamat whitelist berhasil dihapus.');
    }

    // 5. AUDIT SERVER & WEBSHELL SCANNER
    public function server(Request $request, PasaxServerSecurityService $service): View
    {
        $force = $request->boolean('refresh', false);
        return view('keamanan.server', [
            'report' => $service->scan($force),
            'baseline' => $service->baselineSummary(),
            'lockouts' => PasaxLoginAttempt::query()->where('is_locked', true)->latest('id')->get(),
        ]);
    }

    public function serverBaseline(PasaxServerSecurityService $service): RedirectResponse
    {
        $result = $service->createBaseline();
        $total = count($result['files'] ?? []);
        return back()->with('status', "Baseline SHA-256 berhasil dibuat ({$total} berkas dipindai).");
    }

    public function serverBaselineDestroy(PasaxServerSecurityService $service): RedirectResponse
    {
        $service->deleteBaseline();
        return back()->with('status', 'Baseline SHA-256 berhasil dihapus.');
    }

    public function deleteSuspiciousFile(Request $request, PasaxServerSecurityService $service): RedirectResponse
    {
        $request->validate(['path' => ['required', 'string']]);
        $res = $service->deleteSuspiciousFile($request->input('path'));
        if ($res['success']) {
            return back()->with('status', "Berkas mencurigakan '{$request->input('path')}' berhasil dimusnahkan.");
        }
        return back()->withErrors(['err' => $res['message'] ?? 'Gagal menghapus berkas.']);
    }

    public function releaseLockout(int $id): RedirectResponse
    {
        PasaxLoginAttempt::where('id', $id)->delete();
        return back()->with('status', 'Kunci akun login berhasil dibuka kembali.');
    }

    // 6. SESI LOGIN PENGGUNA & IP TERPERCAYA
    public function sessions(Request $request): View
    {
        return view('keamanan.sessions', [
            'sessions' => PasaxUserLogin::query()->latest('id')->paginate(15),
            'trustedIps' => PasaxTrustedIp::query()->latest('id')->get(),
        ]);
    }

    public function destroySession(int $id): RedirectResponse
    {
        $session = PasaxUserLogin::findOrFail($id);
        $session->update(['logout_at' => now()]);
        return back()->with('status', "Sesi pengguna #{$id} berhasil diputus.");
    }

    public function storeTrustedIp(Request $request): RedirectResponse
    {
        $ip = $request->ip() ?: '127.0.0.1';
        PasaxTrustedIp::firstOrCreate([
            'user_id' => $request->user()?->id,
            'ip_address' => $ip,
        ], [
            'device_name' => substr((string) $request->userAgent(), 0, 100),
        ]);

        return back()->with('status', "Alamat IP Anda ({$ip}) berhasil ditandai sebagai IP Terpercaya.");
    }

    public function destroyTrustedIp(int $id): RedirectResponse
    {
        PasaxTrustedIp::destroy($id);
        return back()->with('status', 'IP Terpercaya berhasil dihapus.');
    }

    // 7. TIKET PERMOHONAN BANDING UNBLOCK
    public function tickets(Request $request): View
    {
        $tickets = PasaxIpUnblockRequest::query()
            ->when($request->filled('status'), fn ($q) => $q->where('status', $request->query('status')))
            ->latest('id')
            ->paginate(15)
            ->withQueryString();

        return view('keamanan.tickets', [
            'tickets' => $tickets,
            'filterStatus' => $request->query('status', ''),
        ]);
    }

    public function respondTicket(Request $request, int $id): RedirectResponse
    {
        $validated = $request->validate([
            'status' => ['required', 'in:approved,rejected'],
            'admin_notes' => ['nullable', 'string', 'max:500'],
        ]);

        $ticket = PasaxIpUnblockRequest::findOrFail($id);
        $ticket->update([
            'status' => $validated['status'],
            'admin_notes' => $validated['admin_notes'] ?? null,
            'resolved_by' => $request->user()?->id,
            'resolved_at' => now(),
        ]);

        if ($validated['status'] === 'approved') {
            Pasax::unblockIp($ticket->ip_address, $ticket->device_id);
        }

        $action = $validated['status'] === 'approved' ? 'disetujui dan blokir telah dibuka' : 'ditolak';
        return back()->with('status', "Tiket permohonan #{$ticket->ticket_number} berhasil {$action}.");
    }

    public function destroyTicket(int $id): RedirectResponse
    {
        PasaxIpUnblockRequest::destroy($id);
        return back()->with('status', 'Tiket permohonan berhasil dihapus.');
    }

    // 8. PENGATURAN WAF DINAMIS
    public function settings(Request $request): View
    {
        return view('keamanan.settings', ['settings' => Pasax::getSettings()]);
    }

    public function updateSettings(Request $request): RedirectResponse
    {
        $validated = $request->validate([
            'auto_block_scope' => ['nullable', 'string', 'in:device,ip,auto'],
            'auto_block_enabled' => ['nullable', 'boolean'],
            'auto_block_threshold' => ['nullable', 'integer', 'min:1', 'max:100'],
            'auto_block_duration' => ['nullable', 'integer', 'min:0', 'max:8760'],
            'instant_block_enabled' => ['nullable', 'boolean'],
            'block_enforcement' => ['nullable', 'boolean'],
        ]);

        Pasax::updateSettings($validated);
        return back()->with('status', 'Pengaturan WAF berhasil diperbarui.');
    }
}

5. Implementasi Penuh REST API Controller Host (`app/Http/Controllers/Api/KeamananController.php`)#

Untuk integrasi ke aplikasi mobile (Flutter/React Native), antarmuka Next.js/Nuxt.js, atau sistem pemantauan eksternal:

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Internal\Pasax\Facades\Pasax;
use Internal\Pasax\Models\PasaxAllowedIp;
use Internal\Pasax\Models\PasaxBlockedIp;
use Internal\Pasax\Models\PasaxIpUnblockRequest;
use Internal\Pasax\Models\PasaxLog;
use Internal\Pasax\Models\PasaxLoginAttempt;
use Internal\Pasax\Models\PasaxTrustedIp;
use Internal\Pasax\Models\PasaxUserLogin;
use Internal\Pasax\Services\PasaxServerSecurityService;

class KeamananController extends Controller
{
    // 1. Overview
    public function index(): JsonResponse
    {
        return response()->json([
            'success' => true,
            'message' => 'Status dan statistik operasional WAF.',
            'data' => [
                'waf' => [
                    'enabled' => Pasax::isEnabled(),
                    'block_enforcement' => Pasax::isBlockEnforcementEnabled(),
                ],
                'stats' => Pasax::stats(),
                'level_breakdown' => Pasax::levelBreakdown(24),
                'top_attackers' => Pasax::topAttackers(5),
            ],
        ]);
    }

    // 2. Logs
    public function logs(Request $request): JsonResponse
    {
        $logs = PasaxLog::query()
            ->when($request->query('level'), fn ($q, $l) => $q->where('threat_level', $l))
            ->latest('id')
            ->paginate($request->integer('per_page', 20));

        return response()->json(['success' => true, 'data' => $logs]);
    }

    public function clearLogs(): JsonResponse
    {
        PasaxLog::truncate();
        return response()->json(['success' => true, 'message' => 'Semua riwayat log ancaman WAF berhasil dibersihkan.']);
    }

    // 3. Blocked IPs
    public function blockedIps(): JsonResponse
    {
        $items = PasaxBlockedIp::query()->active()->latest('id')->get();
        return response()->json(['success' => true, 'count' => $items->count(), 'data' => $items]);
    }

    public function checkIp(string $ip): JsonResponse
    {
        return response()->json([
            'success' => true,
            'data' => ['ip_address' => $ip, 'is_blocked' => Pasax::isBlocked($ip)],
        ]);
    }

    public function blockIp(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'ip' => ['required', 'ip'],
            'reason' => ['nullable', 'string', 'max:255'],
            'hours' => ['nullable', 'integer', 'min:1'],
            'scope' => ['nullable', 'string', 'in:ip,device'],
        ]);

        Pasax::blockIp(
            ip: $validated['ip'],
            reason: $validated['reason'] ?? 'Diblokir manual via REST API',
            durationHours: ! empty($validated['hours']) ? (int) $validated['hours'] : null,
            source: 'api',
            userId: $request->user()?->id ? (int) $request->user()->id : null,
            scope: $validated['scope'] ?? 'ip'
        );

        return response()->json(['success' => true, 'message' => "IP {$validated['ip']} berhasil diblokir."], 201);
    }

    public function unblockIp(string $ip): JsonResponse
    {
        $unblocked = Pasax::unblockIp($ip);
        return response()->json([
            'success' => true,
            'message' => $unblocked ? "Blokir untuk IP {$ip} berhasil dibuka." : 'IP tidak ditemukan.',
            'data' => ['is_blocked' => Pasax::isBlocked($ip)],
        ]);
    }

    // 4. Allowed IPs (Whitelist)
    public function allowedIps(): JsonResponse
    {
        $items = PasaxAllowedIp::query()->latest('id')->get();
        return response()->json(['success' => true, 'count' => $items->count(), 'data' => $items]);
    }

    public function storeAllowedIp(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'ip_address' => ['required', 'string', 'max:50'],
            'label' => ['nullable', 'string', 'max:100'],
            'notes' => ['nullable', 'string', 'max:255'],
        ]);

        $item = PasaxAllowedIp::create([
            'ip_address' => $validated['ip_address'],
            'label' => $validated['label'] ?: 'Whitelist API',
            'notes' => $validated['notes'] ?? null,
            'is_active' => true,
            'allowed_by' => $request->user()?->id,
        ]);

        return response()->json(['success' => true, 'message' => "IP {$item->ip_address} ditambahkan ke whitelist.", 'data' => $item], 201);
    }

    public function destroyAllowedIp(int $id): JsonResponse
    {
        PasaxAllowedIp::destroy($id);
        return response()->json(['success' => true, 'message' => "Whitelist #{$id} berhasil dihapus."]);
    }

    // 5. Server Security
    public function serverReport(Request $request, PasaxServerSecurityService $service): JsonResponse
    {
        $force = $request->boolean('refresh', false);
        return response()->json([
            'success' => true,
            'data' => [
                'report' => $service->scan($force),
                'baseline' => $service->baselineSummary(),
            ],
        ]);
    }

    public function createBaseline(PasaxServerSecurityService $service): JsonResponse
    {
        $result = $service->createBaseline();
        return response()->json([
            'success' => true,
            'message' => 'Baseline integritas SHA-256 berhasil dibuat.',
            'data' => [
                'total_files' => count($result['files'] ?? []),
                'created_at' => $result['created_at'] ?? now()->toIso8601String(),
                'files' => $result['files'] ?? [],
            ],
        ], 201);
    }

    // 6. User Sessions & Trusted IPs
    public function sessions(): JsonResponse
    {
        return response()->json([
            'success' => true,
            'data' => [
                'sessions' => PasaxUserLogin::query()->latest('id')->limit(50)->get(),
                'trusted_ips' => PasaxTrustedIp::query()->latest('id')->get(),
            ],
        ]);
    }

    public function storeTrustedIp(Request $request): JsonResponse
    {
        $ip = $request->ip() ?: '127.0.0.1';
        $trusted = PasaxTrustedIp::firstOrCreate([
            'user_id' => $request->user()?->id,
            'ip_address' => $ip,
        ], [
            'device_name' => substr((string) $request->userAgent(), 0, 100),
        ]);

        return response()->json(['success' => true, 'message' => "IP {$ip} disimpan sebagai IP terpercaya.", 'data' => $trusted], 201);
    }

    // 7. Tickets
    public function tickets(Request $request): JsonResponse
    {
        $tickets = PasaxIpUnblockRequest::query()
            ->when($request->query('status'), fn ($q, $s) => $q->where('status', $s))
            ->latest('id')
            ->paginate(15);

        return response()->json(['success' => true, 'data' => $tickets]);
    }

    public function respondTicket(Request $request, int $id): JsonResponse
    {
        $validated = $request->validate([
            'status' => ['required', 'in:approved,rejected'],
            'admin_notes' => ['nullable', 'string', 'max:500'],
        ]);

        $ticket = PasaxIpUnblockRequest::findOrFail($id);
        $ticket->update([
            'status' => $validated['status'],
            'admin_notes' => $validated['admin_notes'] ?? null,
            'resolved_by' => $request->user()?->id,
            'resolved_at' => now(),
        ]);

        if ($validated['status'] === 'approved') {
            Pasax::unblockIp($ticket->ip_address, $ticket->device_id);
        }

        return response()->json(['success' => true, 'message' => "Tiket #{$ticket->ticket_number} diperbarui.", 'data' => $ticket]);
    }

    // 8. Settings
    public function settings(): JsonResponse
    {
        return response()->json(['success' => true, 'data' => Pasax::getSettings()]);
    }

    public function updateSettings(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'auto_block_scope' => ['nullable', 'string', 'in:device,ip,auto'],
            'auto_block_enabled' => ['nullable', 'boolean'],
            'auto_block_threshold' => ['nullable', 'integer', 'min:1', 'max:100'],
            'auto_block_duration' => ['nullable', 'integer', 'min:0', 'max:8760'],
            'instant_block_enabled' => ['nullable', 'boolean'],
            'block_enforcement' => ['nullable', 'boolean'],
        ]);

        $updated = Pasax::updateSettings($validated);
        return response()->json(['success' => true, 'message' => 'Pengaturan WAF berhasil diperbarui.', 'data' => $updated]);
    }
}

6. Mendaftarkan Rute di Aplikasi Host#

Rute Web (`routes/web.php`)#

use App\Http\Controllers\KeamananController;

Route::middleware('auth')->prefix('keamanan')->name('keamanan.')->group(function () {
    // 1. Overview
    Route::get('/', [KeamananController::class, 'index'])->name('index');

    // 2. Logs
    Route::get('logs', [KeamananController::class, 'logs'])->name('logs');
    Route::delete('logs', [KeamananController::class, 'clearLogs'])->name('logs.clear');
    Route::delete('logs/{id}', [KeamananController::class, 'destroyLog'])->name('logs.destroy');

    // 3. Blocked IPs & Quarantine
    Route::get('blocked-ips', [KeamananController::class, 'blockedIps'])->name('blocked-ips');
    Route::post('block', [KeamananController::class, 'blockIp'])->name('block');
    Route::post('unblock/{ip}', [KeamananController::class, 'unblockIp'])->name('unblock');
    Route::patch('blocked-ips/{id}/toggle', [KeamananController::class, 'toggleBlockedIp'])->name('blocked-ips.toggle');
    Route::delete('blocked-ips/{id}', [KeamananController::class, 'destroyBlockedIp'])->name('blocked-ips.destroy');

    // 4. Allowed IPs (Whitelist)
    Route::get('allowed-ips', [KeamananController::class, 'allowedIps'])->name('allowed-ips');
    Route::post('allowed-ips', [KeamananController::class, 'storeAllowedIp'])->name('allowed-ips.store');
    Route::patch('allowed-ips/{id}/toggle', [KeamananController::class, 'toggleAllowedIp'])->name('allowed-ips.toggle');
    Route::delete('allowed-ips/{id}', [KeamananController::class, 'destroyAllowedIp'])->name('allowed-ips.destroy');

    // 5. Server Security & Audit
    Route::get('server', [KeamananController::class, 'server'])->name('server');
    Route::post('server/baseline', [KeamananController::class, 'serverBaseline'])->name('server.baseline');
    Route::delete('server/baseline', [KeamananController::class, 'serverBaselineDestroy'])->name('server.baseline.destroy');
    Route::delete('server/suspicious-files', [KeamananController::class, 'deleteSuspiciousFile'])->name('server.suspicious-files.destroy');
    Route::delete('lockouts/{id}', [KeamananController::class, 'releaseLockout'])->name('lockouts.destroy');

    // 6. User Sessions & Trusted IPs
    Route::get('sessions', [KeamananController::class, 'sessions'])->name('sessions');
    Route::delete('sessions/{id}', [KeamananController::class, 'destroySession'])->name('sessions.destroy');
    Route::post('trusted-ips/my-ip', [KeamananController::class, 'storeTrustedIp'])->name('trusted-ips.store');
    Route::delete('trusted-ips/{id}', [KeamananController::class, 'destroyTrustedIp'])->name('trusted-ips.destroy');

    // 7. Appeal Tickets
    Route::get('tickets', [KeamananController::class, 'tickets'])->name('tickets');
    Route::post('tickets/{id}/respond', [KeamananController::class, 'respondTicket'])->name('tickets.respond');
    Route::delete('tickets/{id}', [KeamananController::class, 'destroyTicket'])->name('tickets.destroy');

    // 8. Runtime Settings
    Route::get('settings', [KeamananController::class, 'settings'])->name('settings');
    Route::post('settings', [KeamananController::class, 'updateSettings'])->name('settings.update');
});

Rute REST API (`routes/api.php`)#

use App\Http\Controllers\Api\KeamananController;

Route::prefix('keamanan')->name('api.keamanan.')->group(function () {
    Route::get('/', [KeamananController::class, 'index'])->name('index');
    Route::get('logs', [KeamananController::class, 'logs'])->name('logs');
    Route::delete('logs', [KeamananController::class, 'clearLogs'])->name('logs.clear');
    Route::get('blocked-ips', [KeamananController::class, 'blockedIps'])->name('blocked-ips');
    Route::get('check/{ip}', [KeamananController::class, 'checkIp'])->name('check');
    Route::post('block', [KeamananController::class, 'blockIp'])->name('block');
    Route::post('unblock/{ip}', [KeamananController::class, 'unblockIp'])->name('unblock');
    Route::get('allowed-ips', [KeamananController::class, 'allowedIps'])->name('allowed-ips');
    Route::post('allowed-ips', [KeamananController::class, 'storeAllowedIp'])->name('allowed-ips.store');
    Route::delete('allowed-ips/{id}', [KeamananController::class, 'destroyAllowedIp'])->name('allowed-ips.destroy');
    Route::get('server', [KeamananController::class, 'serverReport'])->name('server.report');
    Route::post('server/baseline', [KeamananController::class, 'createBaseline'])->name('server.baseline');
    Route::get('sessions', [KeamananController::class, 'sessions'])->name('sessions');
    Route::post('trusted-ips', [KeamananController::class, 'storeTrustedIp'])->name('trusted-ips.store');
    Route::get('tickets', [KeamananController::class, 'tickets'])->name('tickets');
    Route::post('tickets/{id}/respond', [KeamananController::class, 'respondTicket'])->name('tickets.respond');
    Route::get('settings', [KeamananController::class, 'settings'])->name('settings');
    Route::match(['post', 'put'], 'settings', [KeamananController::class, 'updateSettings'])->name('settings.update');
});

7. Pengujian Otomatis (Pest Testing)#

Pastikan implementasi kustom Anda terverifikasi dengan menambahkan pengujian di tests/Feature/KeamananCustomTest.php:

test('authenticated user can access all 8 modular custom views', function () {
    $user = User::factory()->create();

    $routes = [
        '/keamanan',
        '/keamanan/logs',
        '/keamanan/blocked-ips',
        '/keamanan/allowed-ips',
        '/keamanan/server',
        '/keamanan/sessions',
        '/keamanan/tickets',
        '/keamanan/settings',
    ];

    foreach ($routes as $route) {
        $this->actingAs($user)->get($route)->assertOk();
    }
});

Dan jalankan pengujian:

./vendor/bin/pest

Hasil: 100% Pass dengan integritas penuh tanpa pernah menyentuh satu baris kode pun di dalam vendor/!