|
| 1 | +--- |
| 2 | +description: 'Ubah observability bus Deserve jadi jejak audit tingkat kepatuhan, lalu alirkan ke store milik sendiri, sebuah SIEM, atau sebuah WAF.' |
| 3 | +--- |
| 4 | + |
| 5 | +# Audit Kepatuhan |
| 6 | + |
| 7 | +Kerja kepatuhan mengajukan satu pertanyaan sulit ke setiap server: apa yang terjadi, kapan, dan bisakah dibuktikan nanti. Deserve menjawabnya di sumber. Setiap kesalahan subsistem, setiap request yang selesai, dan setiap terminasi diri yang diblokir tiba di satu [observability bus](/id/middleware/observability/overview), terstruktur dan bercap waktu pada saat ia menyala. |
| 8 | + |
| 9 | +Pembingkaian ini penting, jadi layak dinyatakan terang. Deserve bukan [SIEM](https://csrc.nist.gov/glossary/term/security_information_and_event_management) dan tidak lebih tahan lama dari satu. Yang ia berikan adalah *input* SIEM paling rapi yang bisa diserahkan sebuah framework. Data yang meninggalkan bus lebih bersih dan lebih lengkap dari yang dipancarkan kebanyakan framework, karena ia membawa perilaku framework dan kesalahan aplikasi sekaligus, masing-masing di [kanal internal atau external](/id/middleware/observability/events) yang bersih sehingga jalur alert tidak pernah tenggelam dalam lalu lintas rutin. Penyimpanan tahan lama tetap tanggung jawab operator, tapi yang sampai ke penyimpanan itu berangkat jujur. |
| 10 | + |
| 11 | +## Apa yang Sudah Ditangkap Bus |
| 12 | + |
| 13 | +Satu listener [`router.on()`](/id/middleware/observability/overview) melihat seluruh permukaan, dan setiap event berbagi amplop `{ type, kind, metadata, timestamp }` yang sama. Jenis yang paling penting untuk jejak audit memetakan langsung ke hal yang diminta auditor: |
| 14 | + |
| 15 | +| Kebutuhan kepatuhan | Event yang menjawabnya | |
| 16 | +| ---------------------------- | ---------------------------------------------------------------------------------- | |
| 17 | +| Siapa melakukan apa, kapan | [`request:complete`](/id/middleware/observability/events#request) dengan `method`, `url`, `statusCode`, `durationMs`, dan `ip` opsional | |
| 18 | +| Event relevan-keamanan | [`session:invalid`](/id/middleware/observability/events#middleware), [`csrf:rule-error`](/id/middleware/observability/events#middleware), [`process:error`](/id/middleware/observability/events#process) | |
| 19 | +| Kegagalan dan kesalahan | [`request:error`](/id/middleware/observability/events#request), [`worker:crash`](/id/middleware/observability/events#worker), [`view:error`](/id/middleware/observability/events#view) | |
| 20 | +| Garis waktu yang dapat disusun | Setiap event membawa `timestamp` dalam milidetik epoch dan tiba terurut | |
| 21 | + |
| 22 | +Tidak ada yang perlu dikabelkan di dalam handler. Kesalahan menyala sendiri, itulah sebabnya cookie yang dirusak atau `Deno.exit` yang diblokir muncul tanpa satu baris logging pun di rute. Daftar lengkapnya ada di [Referensi Event](/id/middleware/observability/events). |
| 23 | + |
| 24 | +## Listener Tingkat Kepatuhan |
| 25 | + |
| 26 | +Listener audit punya satu tugas: menangkap setiap event sebagai rekaman terstruktur dan menyerahkannya ke penyimpanan tahan lama. Menyaring berdasarkan `type` menjaga kesalahan framework di jalurnya sendiri sambil tetap merekam lalu lintas normal: |
| 27 | + |
| 28 | +```typescript twoslash |
| 29 | +import { Router } from '@neabyte/deserve' |
| 30 | + |
| 31 | +const router = new Router({ |
| 32 | + routesDir: './routes' |
| 33 | +}) |
| 34 | + |
| 35 | +// Satu rekaman audit per event |
| 36 | +router.on((event) => { |
| 37 | + const record = JSON.stringify({ |
| 38 | + at: event.timestamp, |
| 39 | + channel: event.type, |
| 40 | + kind: event.kind, |
| 41 | + ...event.metadata |
| 42 | + }) |
| 43 | + // Event internal memberi makan kanal kesalahan |
| 44 | + if (event.type === 'internal') { |
| 45 | + console.error(record) |
| 46 | + } else { |
| 47 | + console.log(record) |
| 48 | + } |
| 49 | +}) |
| 50 | + |
| 51 | +await router.serve(8000) |
| 52 | +``` |
| 53 | + |
| 54 | +Tiap rekaman sudah JSON, sudah bercap waktu, dan sudah berlabel `channel`. Itu bentuk yang diharapkan setiap hilir di bawah, jadi listener yang sama memberi makan ketiga opsi tanpa perubahan. |
| 55 | + |
| 56 | +## Opsi 1 - Bangun Store Sendiri |
| 57 | + |
| 58 | +Sink tahan lama paling sederhana adalah yang dimiliki dari ujung ke ujung. Tambahkan tiap rekaman ke berkas tulis-saja, kirim ke object storage, atau sisipkan ke database. Sebuah penambah berkas menjaga log audit di disk dan di luar jalur request: |
| 59 | + |
| 60 | +```typescript twoslash |
| 61 | +import { Router } from '@neabyte/deserve' |
| 62 | + |
| 63 | +const router = new Router({ |
| 64 | + routesDir: './routes' |
| 65 | +}) |
| 66 | +// ---cut--- |
| 67 | +// Buka log audit sekali, tambah-saja |
| 68 | +const audit = await Deno.open('./audit.log', { |
| 69 | + create: true, |
| 70 | + append: true |
| 71 | +}) |
| 72 | +const encoder = new TextEncoder() |
| 73 | + |
| 74 | +router.on(async (event) => { |
| 75 | + const record = JSON.stringify({ |
| 76 | + at: event.timestamp, |
| 77 | + ...event |
| 78 | + }) |
| 79 | + // Tambah satu baris per event |
| 80 | + await audit.write(encoder.encode(record + '\n')) |
| 81 | +}) |
| 82 | +``` |
| 83 | + |
| 84 | +Menulis ke disk butuh flag `--allow-write` yang dibatasi ke log, seperti dibahas di [Production Deploy](/id/recipes/production-deploy#mengunci-permission). Untuk retensi jangka panjang, kirim rekaman yang sama ke object storage tahan lama dengan pola di [Object Storage](/id/recipes/object-storage). |
| 85 | + |
| 86 | +## Opsi 2 - Alirkan ke SIEM |
| 87 | + |
| 88 | +Sebuah [SIEM](https://csrc.nist.gov/glossary/term/security_information_and_event_management) mengumpulkan event dari banyak sistem, mengorelasikannya, dan memunculkan alert. Kebanyakan menerima rekaman terstruktur lewat endpoint HTTP biasa, jadi listener audit meneruskan tiap rekaman dengan satu `fetch`: |
| 89 | + |
| 90 | +```typescript twoslash |
| 91 | +import { Router } from '@neabyte/deserve' |
| 92 | + |
| 93 | +const router = new Router({ |
| 94 | + routesDir: './routes' |
| 95 | +}) |
| 96 | +// ---cut--- |
| 97 | +const endpoint = 'https://http-inputs-acme.splunkcloud.com/services/collector/event' |
| 98 | +const token = Deno.env.get('SIEM_TOKEN') ?? '' |
| 99 | + |
| 100 | +router.on((event) => { |
| 101 | + // Teruskan rekaman ke SIEM |
| 102 | + void fetch(endpoint, { |
| 103 | + method: 'POST', |
| 104 | + headers: { |
| 105 | + authorization: `Splunk ${token}`, |
| 106 | + 'content-type': 'application/json' |
| 107 | + }, |
| 108 | + body: JSON.stringify({ |
| 109 | + event: { |
| 110 | + ...event.metadata, |
| 111 | + kind: event.kind |
| 112 | + } |
| 113 | + }) |
| 114 | + }) |
| 115 | +}) |
| 116 | +``` |
| 117 | + |
| 118 | +Endpoint dan bentuk auth mengikuti vendor. Kolektor umum dengan API ingest HTTP publik mencakup [Splunk HTTP Event Collector](https://docs.splunk.com/Documentation/Splunk/latest/Data/UsetheHTTPEventCollector), [Datadog Logs Intake](https://docs.datadoghq.com/api/latest/logs/), [Elasticsearch Bulk API](https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-bulk), dan endpoint [OpenTelemetry OTLP/HTTP](https://opentelemetry.io/docs/specs/otel/protocol/exporter/) mana pun. `fetch` keluar butuh flag `--allow-net` dari [Production Deploy](/id/recipes/production-deploy#daftar-periksa-permission), dan panggilannya ditembakkan tanpa `await` supaya jalur request tetap cepat. |
| 119 | + |
| 120 | +## Opsi 3 - Beri Makan Loop Keputusan WAF |
| 121 | + |
| 122 | +Sebuah [Web Application Firewall](https://owasp.org/www-community/Web_Application_Firewall) memblokir lalu lintas buruk sebelum sampai ke aplikasi, dan bus memberinya sinyal untuk bertindak. Lonjakan event `request:error` dari satu `ip`, atau kesalahan `csrf:rule-error` berulang, persis pola yang diincar aturan WAF. Teruskan jenis yang relevan-keamanan ke API firewall untuk menggerakkan daftar blokir: |
| 123 | + |
| 124 | +```typescript twoslash |
| 125 | +import { Router } from '@neabyte/deserve' |
| 126 | + |
| 127 | +const router = new Router({ |
| 128 | + routesDir: './routes' |
| 129 | +}) |
| 130 | +// ---cut--- |
| 131 | +router.on((event) => { |
| 132 | + // Hanya teruskan kesalahan relevan-keamanan |
| 133 | + if (event.kind === 'csrf:rule-error' || event.kind === 'request:error') { |
| 134 | + void fetch('https://waf.internal/signals', { |
| 135 | + method: 'POST', |
| 136 | + headers: { |
| 137 | + 'content-type': 'application/json' |
| 138 | + }, |
| 139 | + body: JSON.stringify({ |
| 140 | + at: event.timestamp, |
| 141 | + ...event.metadata |
| 142 | + }) |
| 143 | + }) |
| 144 | + } |
| 145 | +}) |
| 146 | +``` |
| 147 | + |
| 148 | +Firewall terkelola membukanya lewat API masing-masing, seperti [aturan kustom WAF Cloudflare](https://developers.cloudflare.com/waf/custom-rules/) atau [API AWS WAF](https://docs.aws.amazon.com/waf/latest/APIReference/Welcome.html). Bus memasok bukti, WAF memegang putusan, dan keduanya tetap terpisah bersih. |
| 149 | + |
| 150 | +## Batas yang Jujur |
| 151 | + |
| 152 | +Menjaga klaim tetap lurus membuat resep ini tepercaya: |
| 153 | + |
| 154 | +- **Tidak tahan lama sendirian.** Tanpa listener terdaftar, emit jadi no-op, jadi kesalahan sebelum penyimpanan dikabelkan memang tidak terekam. Ketahanan tinggal di sink, bukan di bus. |
| 155 | +- **Upaya terbaik, dalam proses.** Event menyala real time di server, jadi crash keras antara emit dan tulis bisa menjatuhkan rekaman terakhir. [Process guard](/id/error-handling/defense-in-depth#lapis-5-process-guard) menjaga proses tetap hidup melewati sebagian besar kesalahan, yang mempersempit jendela itu tapi tidak menutupnya. |
| 156 | +- **Input, bukan analisis.** Bus menghasilkan rekaman bersih. Korelasi, retensi, dan alerting milik store, SIEM, atau WAF yang menerimanya. |
| 157 | + |
| 158 | +Yang dijamin Deserve adalah bagian yang biasanya gagal dibuat framework: data yang sampai ke penyimpanan itu terstruktur, bercap waktu di sumber, terpisah per kanal, dan lengkap di seluruh perilaku framework dan kesalahan aplikasi. Semua dapat diaudit karena semua memancar. |
| 159 | + |
0 commit comments