Skip to content

Commit 096d226

Browse files
committed
docs(recipes): add audit compliance recipe
- Add audit compliance recipe in English and Indonesian - Register the recipe in both locale sidebars
1 parent 7c8b153 commit 096d226

3 files changed

Lines changed: 321 additions & 2 deletions

File tree

docs/.vitepress/config.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,8 @@ export default withMermaid(
248248
{ text: 'Streaming Data', link: '/recipes/streaming-data' },
249249
{ text: 'Object Storage', link: '/recipes/object-storage' },
250250
{ text: 'Graceful Shutdown', link: '/recipes/graceful-shutdown' },
251-
{ text: 'Production Deploy', link: '/recipes/production-deploy' }
251+
{ text: 'Production Deploy', link: '/recipes/production-deploy' },
252+
{ text: 'Audit Compliance', link: '/recipes/audit-compliance' }
252253
]
253254
}
254255
]
@@ -438,7 +439,8 @@ export default withMermaid(
438439
{ text: 'Streaming Data', link: '/id/recipes/streaming-data' },
439440
{ text: 'Object Storage', link: '/id/recipes/object-storage' },
440441
{ text: 'Graceful Shutdown', link: '/id/recipes/graceful-shutdown' },
441-
{ text: 'Production Deploy', link: '/id/recipes/production-deploy' }
442+
{ text: 'Production Deploy', link: '/id/recipes/production-deploy' },
443+
{ text: 'Audit Kepatuhan', link: '/id/recipes/audit-compliance' }
442444
]
443445
}
444446
]
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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+

docs/recipes/audit-compliance.md

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
---
2+
description: 'Turn the Deserve observability bus into a compliance-grade audit trail, then pipe it to your own store, a SIEM, or a WAF.'
3+
---
4+
5+
# Audit Compliance
6+
7+
Compliance work asks a hard question of every server: what happened, when, and can it be proven later. Deserve answers it at the source. Every subsystem fault, every finished request, and every blocked self-termination arrives on one [observability bus](/middleware/observability/overview), structured and timestamped the instant it fires.
8+
9+
That framing matters, so it is worth stating plainly. Deserve is not a [SIEM](https://csrc.nist.gov/glossary/term/security_information_and_event_management) and is not more durable than one. What it is, is the best-behaved SIEM *input* a framework can hand over. The data leaving the bus is cleaner and more complete than most frameworks emit, because it carries framework behaviour and application faults alike, each on a clean [internal or external channel](/middleware/observability/events) so the alert path never drowns in routine traffic. Durable storage is still on the operator, but what reaches that storage starts honest.
10+
11+
## What the Bus Already Captures
12+
13+
A single [`router.on()`](/middleware/observability/overview) listener sees the whole surface, and every event shares the same `{ type, kind, metadata, timestamp }` envelope. The kinds that matter most for an audit trail map straight onto what auditors ask for:
14+
15+
| Compliance need | Events that answer it |
16+
| ---------------------------- | ---------------------------------------------------------------------------------- |
17+
| Who did what, when | [`request:complete`](/middleware/observability/events#requests) with `method`, `url`, `statusCode`, `durationMs`, and optional `ip` |
18+
| Security-relevant events | [`session:invalid`](/middleware/observability/events#middleware), [`csrf:rule-error`](/middleware/observability/events#middleware), [`process:error`](/middleware/observability/events#process) |
19+
| Failures and faults | [`request:error`](/middleware/observability/events#requests), [`worker:crash`](/middleware/observability/events#workers), [`view:error`](/middleware/observability/events#views) |
20+
| Reconstructable timeline | Every event carries a `timestamp` in epoch milliseconds and arrives in order |
21+
22+
Nothing here needs wiring inside handlers. The faults emit on their own, which is why a tampered cookie or a blocked `Deno.exit` shows up without a single line of logging in the route. The full list lives in the [Event Reference](/middleware/observability/events).
23+
24+
## A Compliance-Grade Listener
25+
26+
The audit listener has one job: capture every event as a structured record and hand it to durable storage. Filtering on `type` keeps framework faults on their own track while still recording normal traffic:
27+
28+
```typescript twoslash
29+
import { Router } from '@neabyte/deserve'
30+
31+
const router = new Router({
32+
routesDir: './routes'
33+
})
34+
35+
// One audit record 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+
// Internal events feed the fault channel
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+
Each record is already JSON, already timestamped, and already labelled by `channel`. That is the shape every downstream below expects, so the same listener feeds all three options without change.
55+
56+
## Option 1 - Build Your Own Store
57+
58+
The simplest durable sink is one owned end to end. Append each record to a write-only file, ship it to object storage, or insert it into a database. A file appender keeps the audit log on disk and out of the request path:
59+
60+
```typescript twoslash
61+
import { Router } from '@neabyte/deserve'
62+
63+
const router = new Router({
64+
routesDir: './routes'
65+
})
66+
// ---cut---
67+
// Open the audit log once, append-only
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+
// Append one line per event
80+
await audit.write(encoder.encode(record + '\n'))
81+
})
82+
```
83+
84+
Writing to disk needs the `--allow-write` flag scoped to the log, as covered in [Production Deploy](/recipes/production-deploy#locking-permissions-down). For long-term retention, ship the same records to durable object storage with the pattern in [Object Storage](/recipes/object-storage).
85+
86+
## Option 2 - Stream to a SIEM
87+
88+
A [SIEM](https://csrc.nist.gov/glossary/term/security_information_and_event_management) collects events from many systems, correlates them, and raises alerts. Most accept structured records over a plain HTTP endpoint, so the audit listener forwards each record with a single `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+
// Forward the record to the 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+
The endpoint and auth shape follow the vendor. Common collectors with public HTTP ingest APIs include [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), and any [OpenTelemetry OTLP/HTTP](https://opentelemetry.io/docs/specs/otel/protocol/exporter/) endpoint. Outbound `fetch` needs the `--allow-net` flag from [Production Deploy](/recipes/production-deploy#permission-checklist), and the call is fired without `await` so the request path stays fast.
119+
120+
## Option 3 - Feed a WAF Decision Loop
121+
122+
A [Web Application Firewall](https://owasp.org/www-community/Web_Application_Firewall) blocks bad traffic before it reaches the app, and the bus gives it signal to act on. A burst of `request:error` events from one `ip`, or repeated `csrf:rule-error` faults, is exactly the pattern a WAF rule wants. Forward the security-relevant kinds to the firewall's API to drive a block list:
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+
// Only forward security-relevant faults
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+
Managed firewalls expose this through their own APIs, such as [Cloudflare WAF custom rules](https://developers.cloudflare.com/waf/custom-rules/) or the [AWS WAF API](https://docs.aws.amazon.com/waf/latest/APIReference/Welcome.html). The bus supplies the evidence, the WAF owns the verdict, and the two stay cleanly separated.
149+
150+
## Honest Limits
151+
152+
Holding the claim straight keeps the recipe trustworthy:
153+
154+
- **Not durable on its own.** With no listener registered, emitting is a no-op, so a fault before storage is wired is simply not recorded. Durability lives in the sink, not the bus.
155+
- **Best effort, in process.** Events fire in real time on the server, so a hard crash between emit and write can drop the last record. The [process guard](/error-handling/defense-in-depth#layer-5-process-guard) keeps the process alive through most faults, which narrows that window but does not close it.
156+
- **Input, not analysis.** The bus produces clean records. Correlation, retention, and alerting belong to whatever store, SIEM, or WAF receives them.
157+
158+
What Deserve guarantees is the part frameworks usually get wrong: the data arriving at storage is structured, timestamped at the source, split by channel, and complete across framework behaviour and application faults. Everything is auditable because everything emits.

0 commit comments

Comments
 (0)