@queno/agent-node 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +421 -0
  2. package/dist/agent.d.ts +222 -0
  3. package/dist/agent.d.ts.map +1 -0
  4. package/dist/agent.js +591 -0
  5. package/dist/agent.js.map +1 -0
  6. package/dist/api-discovery/discovery-buffer.d.ts +27 -0
  7. package/dist/api-discovery/discovery-buffer.d.ts.map +1 -0
  8. package/dist/api-discovery/discovery-buffer.js +50 -0
  9. package/dist/api-discovery/discovery-buffer.js.map +1 -0
  10. package/dist/api-discovery/endpoint-observer.d.ts +25 -0
  11. package/dist/api-discovery/endpoint-observer.d.ts.map +1 -0
  12. package/dist/api-discovery/endpoint-observer.js +127 -0
  13. package/dist/api-discovery/endpoint-observer.js.map +1 -0
  14. package/dist/api-discovery/route-normalizer.d.ts +15 -0
  15. package/dist/api-discovery/route-normalizer.d.ts.map +1 -0
  16. package/dist/api-discovery/route-normalizer.js +34 -0
  17. package/dist/api-discovery/route-normalizer.js.map +1 -0
  18. package/dist/config.d.ts +100 -0
  19. package/dist/config.d.ts.map +1 -0
  20. package/dist/config.js +101 -0
  21. package/dist/config.js.map +1 -0
  22. package/dist/db-hooks/correlate.d.ts +19 -0
  23. package/dist/db-hooks/correlate.d.ts.map +1 -0
  24. package/dist/db-hooks/correlate.js +45 -0
  25. package/dist/db-hooks/correlate.js.map +1 -0
  26. package/dist/db-hooks/instrument.d.ts +27 -0
  27. package/dist/db-hooks/instrument.d.ts.map +1 -0
  28. package/dist/db-hooks/instrument.js +194 -0
  29. package/dist/db-hooks/instrument.js.map +1 -0
  30. package/dist/detectors/base.d.ts +61 -0
  31. package/dist/detectors/base.d.ts.map +1 -0
  32. package/dist/detectors/base.js +57 -0
  33. package/dist/detectors/base.js.map +1 -0
  34. package/dist/detectors/bola.d.ts +60 -0
  35. package/dist/detectors/bola.d.ts.map +1 -0
  36. package/dist/detectors/bola.js +108 -0
  37. package/dist/detectors/bola.js.map +1 -0
  38. package/dist/detectors/command-injection.d.ts +22 -0
  39. package/dist/detectors/command-injection.d.ts.map +1 -0
  40. package/dist/detectors/command-injection.js +41 -0
  41. package/dist/detectors/command-injection.js.map +1 -0
  42. package/dist/detectors/custom-rule.d.ts +24 -0
  43. package/dist/detectors/custom-rule.d.ts.map +1 -0
  44. package/dist/detectors/custom-rule.js +65 -0
  45. package/dist/detectors/custom-rule.js.map +1 -0
  46. package/dist/detectors/index.d.ts +17 -0
  47. package/dist/detectors/index.d.ts.map +1 -0
  48. package/dist/detectors/index.js +31 -0
  49. package/dist/detectors/index.js.map +1 -0
  50. package/dist/detectors/nosql-injection.d.ts +23 -0
  51. package/dist/detectors/nosql-injection.d.ts.map +1 -0
  52. package/dist/detectors/nosql-injection.js +54 -0
  53. package/dist/detectors/nosql-injection.js.map +1 -0
  54. package/dist/detectors/path-traversal.d.ts +21 -0
  55. package/dist/detectors/path-traversal.d.ts.map +1 -0
  56. package/dist/detectors/path-traversal.js +54 -0
  57. package/dist/detectors/path-traversal.js.map +1 -0
  58. package/dist/detectors/prototype-pollution.d.ts +23 -0
  59. package/dist/detectors/prototype-pollution.d.ts.map +1 -0
  60. package/dist/detectors/prototype-pollution.js +50 -0
  61. package/dist/detectors/prototype-pollution.js.map +1 -0
  62. package/dist/detectors/sql-injection.d.ts +22 -0
  63. package/dist/detectors/sql-injection.d.ts.map +1 -0
  64. package/dist/detectors/sql-injection.js +42 -0
  65. package/dist/detectors/sql-injection.js.map +1 -0
  66. package/dist/detectors/ssrf.d.ts +26 -0
  67. package/dist/detectors/ssrf.d.ts.map +1 -0
  68. package/dist/detectors/ssrf.js +37 -0
  69. package/dist/detectors/ssrf.js.map +1 -0
  70. package/dist/detectors/suspicious-headers.d.ts +25 -0
  71. package/dist/detectors/suspicious-headers.d.ts.map +1 -0
  72. package/dist/detectors/suspicious-headers.js +87 -0
  73. package/dist/detectors/suspicious-headers.js.map +1 -0
  74. package/dist/detectors/template-injection.d.ts +27 -0
  75. package/dist/detectors/template-injection.d.ts.map +1 -0
  76. package/dist/detectors/template-injection.js +35 -0
  77. package/dist/detectors/template-injection.js.map +1 -0
  78. package/dist/detectors/xss.d.ts +22 -0
  79. package/dist/detectors/xss.d.ts.map +1 -0
  80. package/dist/detectors/xss.js +38 -0
  81. package/dist/detectors/xss.js.map +1 -0
  82. package/dist/index.d.ts +28 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +24 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/integrations/express.d.ts +39 -0
  87. package/dist/integrations/express.d.ts.map +1 -0
  88. package/dist/integrations/express.js +62 -0
  89. package/dist/integrations/express.js.map +1 -0
  90. package/dist/integrations/fastify.d.ts +33 -0
  91. package/dist/integrations/fastify.d.ts.map +1 -0
  92. package/dist/integrations/fastify.js +63 -0
  93. package/dist/integrations/fastify.js.map +1 -0
  94. package/dist/integrations/nestjs.d.ts +40 -0
  95. package/dist/integrations/nestjs.d.ts.map +1 -0
  96. package/dist/integrations/nestjs.js +58 -0
  97. package/dist/integrations/nestjs.js.map +1 -0
  98. package/dist/policy/canonical.d.ts +23 -0
  99. package/dist/policy/canonical.d.ts.map +1 -0
  100. package/dist/policy/canonical.js +40 -0
  101. package/dist/policy/canonical.js.map +1 -0
  102. package/dist/policy/policy-manager.d.ts +43 -0
  103. package/dist/policy/policy-manager.d.ts.map +1 -0
  104. package/dist/policy/policy-manager.js +89 -0
  105. package/dist/policy/policy-manager.js.map +1 -0
  106. package/dist/policy/types.d.ts +70 -0
  107. package/dist/policy/types.d.ts.map +1 -0
  108. package/dist/policy/types.js +2 -0
  109. package/dist/policy/types.js.map +1 -0
  110. package/dist/policy/verify.d.ts +11 -0
  111. package/dist/policy/verify.d.ts.map +1 -0
  112. package/dist/policy/verify.js +61 -0
  113. package/dist/policy/verify.js.map +1 -0
  114. package/dist/redaction/audit-log.d.ts +40 -0
  115. package/dist/redaction/audit-log.d.ts.map +1 -0
  116. package/dist/redaction/audit-log.js +110 -0
  117. package/dist/redaction/audit-log.js.map +1 -0
  118. package/dist/redaction/engine.d.ts +50 -0
  119. package/dist/redaction/engine.d.ts.map +1 -0
  120. package/dist/redaction/engine.js +143 -0
  121. package/dist/redaction/engine.js.map +1 -0
  122. package/dist/redaction/patterns.d.ts +24 -0
  123. package/dist/redaction/patterns.d.ts.map +1 -0
  124. package/dist/redaction/patterns.js +142 -0
  125. package/dist/redaction/patterns.js.map +1 -0
  126. package/dist/runtime-context.d.ts +33 -0
  127. package/dist/runtime-context.d.ts.map +1 -0
  128. package/dist/runtime-context.js +46 -0
  129. package/dist/runtime-context.js.map +1 -0
  130. package/dist/self-protect.d.ts +34 -0
  131. package/dist/self-protect.d.ts.map +1 -0
  132. package/dist/self-protect.js +134 -0
  133. package/dist/self-protect.js.map +1 -0
  134. package/dist/transport/buffer.d.ts +52 -0
  135. package/dist/transport/buffer.d.ts.map +1 -0
  136. package/dist/transport/buffer.js +57 -0
  137. package/dist/transport/buffer.js.map +1 -0
  138. package/dist/transport/client.d.ts +77 -0
  139. package/dist/transport/client.d.ts.map +1 -0
  140. package/dist/transport/client.js +178 -0
  141. package/dist/transport/client.js.map +1 -0
  142. package/dist/transport/heartbeat.d.ts +86 -0
  143. package/dist/transport/heartbeat.d.ts.map +1 -0
  144. package/dist/transport/heartbeat.js +110 -0
  145. package/dist/transport/heartbeat.js.map +1 -0
  146. package/dist/transport/secure-request.d.ts +30 -0
  147. package/dist/transport/secure-request.d.ts.map +1 -0
  148. package/dist/transport/secure-request.js +95 -0
  149. package/dist/transport/secure-request.js.map +1 -0
  150. package/dist/types.d.ts +311 -0
  151. package/dist/types.d.ts.map +1 -0
  152. package/dist/types.js +12 -0
  153. package/dist/types.js.map +1 -0
  154. package/package.json +60 -0
package/README.md ADDED
@@ -0,0 +1,421 @@
1
+ # @queno/agent-node
2
+
3
+ > Runtime Application Self-Protection for Node.js - Express · Fastify · NestJS
4
+
5
+ [![npm version](https://img.shields.io/badge/npm-0.1.2-blue)](https://www.npmjs.com/package/@queno/agent-node)
6
+ [![node](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
7
+ [![license](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
8
+ [![build](https://img.shields.io/badge/build-passing-brightgreen)](#)
9
+
10
+ `@queno/agent-node` is a lightweight RASP agent that installs inside your Node.js application and **detects, redacts, audits, and optionally blocks** runtime attacks - without a proxy, without a WAF, and without crashing your app.
11
+
12
+ It instruments Express, Fastify, and NestJS middleware to inspect each incoming request against 10 built-in attack detectors. Sensitive data is redacted **before leaving the process**, a tamper-evident audit log is written locally, and security events are forwarded to the RASP collector.
13
+
14
+ ---
15
+
16
+ ## Features
17
+
18
+ - **10 built-in detectors** - SQL injection, XSS, Command injection, Path traversal, NoSQL injection, SSRF, Prototype pollution, Template injection, Suspicious headers, BOLA/IDOR
19
+ - **Local redaction engine** - key-based denylist + value-based (email, card, SIN/RAMQ, IP). Redaction happens *before* telemetry leaves the process
20
+ - **Local JSONL audit log** - metadata-only, never raw sensitive values, with automatic rotation
21
+ - **Monitor and block modes** - monitor by default; block must be explicitly enabled or pushed via signed policy
22
+ - **Signed policy distribution** - Ed25519 signed configuration from the dashboard (mode, custom rules, redaction config). Agents verify before applying
23
+ - **Passive API discovery** - observes endpoints at runtime, normalizes routes, flushes inventory to the collector
24
+ - **Self-protection (optional)** - AES-256-GCM in-memory secret store, anti-debug detection, DB hook integrity
25
+ - **Fail-open** - any internal error is swallowed; the agent never crashes the host application
26
+ - **Single runtime dependency** - only `zod`. No axios, no winston, no lodash
27
+
28
+ ---
29
+
30
+ ## Requirements
31
+
32
+ | Requirement | Version |
33
+ |---|---|
34
+ | Node.js | **≥ 18** |
35
+ | TypeScript | ≥ 5 (optional) |
36
+ | Framework | Express ≥ 4, Fastify ≥ 4, or NestJS ≥ 10 |
37
+
38
+ > Node ≥ 18 is required for native `crypto` (AES-GCM, Ed25519, HMAC), native `fetch`, and `AsyncLocalStorage`.
39
+
40
+ ---
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ # npm
46
+ npm install @queno/agent-node
47
+
48
+ # pnpm
49
+ pnpm add @queno/agent-node
50
+
51
+ # yarn
52
+ yarn add @queno/agent-node
53
+ ```
54
+
55
+ Install the peer dependency for your framework - only what you need:
56
+
57
+ ```bash
58
+ # Express
59
+ npm install express
60
+
61
+ # Fastify
62
+ npm install fastify
63
+
64
+ # NestJS
65
+ npm install @nestjs/common @nestjs/core
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Quickstart
71
+
72
+ Before you start: create a **Project** and an **Agent** in the RASP dashboard, then copy the `apiKey`, `projectId`, and `agentId`.
73
+
74
+ ### Express
75
+
76
+ ```typescript
77
+ import express from "express";
78
+ import { RaspAgent, createExpressMiddleware } from "@queno/agent-node";
79
+
80
+ // 1. Create and start the agent
81
+ const agent = new RaspAgent({
82
+ apiKey: process.env.RASP_API_KEY!,
83
+ projectId: process.env.RASP_PROJECT_ID!,
84
+ agentId: process.env.RASP_AGENT_ID!,
85
+ framework: "express",
86
+ mode: "monitor", // "monitor" | "block"
87
+ });
88
+ agent.start();
89
+
90
+ const app = express();
91
+ app.use(express.json());
92
+ app.use(express.urlencoded({ extended: false }));
93
+
94
+ // 2. Mount the RASP middleware AFTER body parsers
95
+ app.use(createExpressMiddleware(agent));
96
+
97
+ // 3. Your routes
98
+ app.get("/api/users/:id", (req, res) => {
99
+ // In block mode, the middleware already replied 403 if an attack was detected
100
+ res.json({ id: req.params.id });
101
+ });
102
+
103
+ // 4. Drain on shutdown
104
+ process.on("SIGTERM", async () => {
105
+ await agent.stop(); // flushes event buffer + closes audit log
106
+ process.exit(0);
107
+ });
108
+
109
+ app.listen(3000);
110
+ ```
111
+
112
+ ### Fastify
113
+
114
+ ```typescript
115
+ import Fastify from "fastify";
116
+ import { RaspAgent, createFastifyPlugin } from "@queno/agent-node";
117
+
118
+ const agent = new RaspAgent({
119
+ apiKey: process.env.RASP_API_KEY!,
120
+ projectId: process.env.RASP_PROJECT_ID!,
121
+ agentId: process.env.RASP_AGENT_ID!,
122
+ framework: "fastify",
123
+ });
124
+ agent.start();
125
+
126
+ const fastify = Fastify({ logger: true });
127
+
128
+ // Register as a Fastify plugin
129
+ await fastify.register(createFastifyPlugin(agent));
130
+
131
+ fastify.get("/api/items", async () => ({ items: [] }));
132
+
133
+ await fastify.listen({ port: 3000 });
134
+
135
+ process.on("SIGTERM", async () => {
136
+ await fastify.close();
137
+ await agent.stop();
138
+ });
139
+ ```
140
+
141
+ > **Note:** The Fastify plugin hooks into `onRequest` (before body parsing). To inspect request bodies, add a `preHandler` hook after plugin registration.
142
+
143
+ ### NestJS
144
+
145
+ ```typescript
146
+ // rasp.module.ts
147
+ import { Module, NestModule, MiddlewareConsumer } from "@nestjs/common";
148
+ import { RaspAgent, createNestMiddleware } from "@queno/agent-node";
149
+
150
+ export const raspAgent = new RaspAgent({
151
+ apiKey: process.env.RASP_API_KEY!,
152
+ projectId: process.env.RASP_PROJECT_ID!,
153
+ agentId: process.env.RASP_AGENT_ID!,
154
+ framework: "nestjs",
155
+ });
156
+ raspAgent.start();
157
+
158
+ @Module({ imports: [/* your modules */] })
159
+ export class AppModule implements NestModule {
160
+ configure(consumer: MiddlewareConsumer) {
161
+ consumer
162
+ .apply(createNestMiddleware(raspAgent))
163
+ .forRoutes("*");
164
+ }
165
+ }
166
+ ```
167
+
168
+ ```typescript
169
+ // main.ts - graceful shutdown
170
+ import { raspAgent } from "./rasp.module";
171
+
172
+ process.on("SIGTERM", async () => {
173
+ await raspAgent.stop();
174
+ });
175
+ ```
176
+
177
+ ---
178
+
179
+ ## Configuration
180
+
181
+ Pass a `RaspConfig` object to the `RaspAgent` constructor. All fields except `apiKey`, `projectId`, and `agentId` are optional with safe defaults.
182
+
183
+ | Option | Type | Default | Min | Description |
184
+ |---|---|---|---|---|
185
+ | `apiKey` * | `string` | - | - | API key generated in the dashboard |
186
+ | `projectId` * | `string` | - | - | Project ID |
187
+ | `agentId` * | `string` | - | - | Agent ID (registered in the dashboard) |
188
+ | `mode` | `"monitor" \| "block"` | `"monitor"` | - | Initial mode. Can be overridden by a signed policy |
189
+ | `channel` | `"stable" \| "early" \| "edge"` | `"stable"` | - | Policy and version distribution channel |
190
+ | `heartbeatIntervalMs` | `number` | `30000` | `5000` | Heartbeat interval |
191
+ | `flushIntervalMs` | `number` | `5000` | `1000` | Event buffer flush interval |
192
+ | `bufferMaxSize` | `number` | `50` | `1` | Flush also triggers at this buffer size |
193
+ | `transportTimeoutMs` | `number` | `5000` | `500` | HTTP timeout to the collector |
194
+ | `auditLog` | `boolean` | `true` | - | Enable local JSONL audit log |
195
+ | `auditLogPath` | `string` | `"./rasp-audit.log"` | - | Local audit log path |
196
+ | `auditLogMaxBytes` | `number` | `10485760` | `1` | Rotation size (10 MB default) |
197
+ | `discoveryFlushIntervalMs` | `number` | `60000` | `5000` | API discovery flush interval |
198
+ | `hmacSecret` | `string` | - | - | HMAC-SHA256 secret if `HMAC_REQUIRED=true` on the collector |
199
+ | `instrumentDb` | `boolean` | `false` | - | Enable DB driver monkey-patching for BOLA correlation (`pg`, `mysql2`, `mongoose`, `knex`, `sequelize`, Prisma) |
200
+ | `selfProtect` | `boolean` | `false` | - | Enable anti-debug, hook integrity checks, extended SecureStore |
201
+ | `policyPublicKey` | `string` | Dev key (built-in) | - | Ed25519 PEM public key. **Override in production** |
202
+ | `agentVersion` | `string` | - | - | Reported in heartbeats |
203
+ | `framework` | `string` | - | - | Free-form tag reported in heartbeats |
204
+ | `tls` | `object` | - | - | TLS/mTLS: `caCert`, `clientCert`, `clientKey`, `collectorFingerprints`, `rejectUnauthorized` |
205
+
206
+ ### Environment variables
207
+
208
+ Two environment variables are read from `process.env` by `src/config.ts`:
209
+
210
+ | Variable | Default | Description |
211
+ |---|---|---|
212
+ | `RASP_COLLECTOR_URL` | `https://collector.rasp.dev` | Override the collector URL (dev/staging) |
213
+ | `RASP_POLICY_PUBLIC_KEY` | Built-in dev key | Override the trusted Ed25519 public key |
214
+
215
+ All other options are passed directly via `RaspConfig` - not read from the environment.
216
+
217
+ ---
218
+
219
+ ## Modes
220
+
221
+ ### Monitor (default)
222
+
223
+ All requests pass through. Detections are reported as events to the collector and visible in the dashboard. **No impact on your application's responses.**
224
+
225
+ ```typescript
226
+ const agent = new RaspAgent({ ..., mode: "monitor" });
227
+ ```
228
+
229
+ ### Block
230
+
231
+ When an attack is detected, the middleware replies with `HTTP 403` and does **not** call `next()`:
232
+
233
+ ```json
234
+ { "error": "Request blocked by RASP", "eventType": "sql_injection" }
235
+ ```
236
+
237
+ ```typescript
238
+ const agent = new RaspAgent({ ..., mode: "block" });
239
+ ```
240
+
241
+ > **Important:** Always read `agent.mode` at runtime - the mode may change via a signed policy pushed from the dashboard. Do not rely on the boot config value.
242
+
243
+ ### Kill-switch
244
+
245
+ If the dashboard activates a kill-switch (per-agent or global), the agent's `inspect()` becomes a no-op on the next heartbeat. The agent stops sending telemetry but continues heartbeating to detect when the kill-switch is lifted. **Recovery is automatic.**
246
+
247
+ ---
248
+
249
+ ## Detectors
250
+
251
+ | # | Detector | Severity | Targets |
252
+ |---|---|---|---|
253
+ | 1 | SQL injection | critical | query, body, path |
254
+ | 2 | XSS | high | query, body |
255
+ | 3 | Command injection | critical | query, body, path |
256
+ | 4 | Path traversal | high | query, body, path |
257
+ | 5 | NoSQL injection | high | query, body |
258
+ | 6 | SSRF | high | query, body |
259
+ | 7 | Prototype pollution | critical | query, body |
260
+ | 8 | Template injection | high | query, body |
261
+ | 9 | Suspicious headers | medium/high | headers |
262
+ | 10 | BOLA / IDOR | high | path, JWT `sub` |
263
+
264
+ Detection is **synchronous** on the request path. All telemetry side-effects (redaction, audit, buffer, transport) are **async fire-and-forget**.
265
+
266
+ ---
267
+
268
+ ## Security & Privacy
269
+
270
+ ### Redaction before transport
271
+
272
+ The redaction engine runs **before** any event enters the transport buffer. If redaction throws, the event is **dropped** (fail-closed) and logged locally with `dropped: true`. No raw sensitive value ever leaves the process.
273
+
274
+ **Default key redaction:** `authorization`, `cookie`, `password`, `secret`, `token`, `api_key`, `ssn`, `credit_card`, `cvv`, `pin`, `x-api-key` and more.
275
+
276
+ **Value-based redaction:** emails → `[EMAIL:<hash>]`, Luhn-valid cards → `****-****-****-XXXX`, SIN → `[SIN REDACTED]`, RAMQ → `[HEALTH_ID REDACTED]`, SQL literals → `[STRING]`/`[INT]`.
277
+
278
+ ### Local audit log
279
+
280
+ Every redaction action is journaled locally as JSONL (`./rasp-audit.log` by default). Contents: timestamp, eventType, severity, redactedFields - **never raw values**. The log never leaves the customer's environment.
281
+
282
+ ### Secrets in memory
283
+
284
+ `apiKey` and `hmacSecret` are stored encrypted in memory using `SecureStore` (AES-256-GCM, per-process random key). They never appear in heap dumps or crash logs.
285
+
286
+ ### Signed policy
287
+
288
+ The agent verifies every policy with Ed25519 before applying it. Policies are monotonically versioned - replay attacks are rejected. Without a valid signature, the agent keeps its last known-good policy.
289
+
290
+ ### Transport hardening
291
+
292
+ - **HMAC-SHA256** payload signing (`X-RASP-Signature` header) - opt-in via `hmacSecret`
293
+ - **TLS certificate pinning** - via `tls.collectorFingerprints` (SHA-256 DER)
294
+ - **mTLS** - via `tls.clientCert` + `tls.clientKey`
295
+
296
+ ### Fail-open (never crash your app)
297
+
298
+ Every exception inside the agent - in detectors, transport, audit log - is caught and swallowed. The agent never propagates errors to the host application.
299
+
300
+ ---
301
+
302
+ ## Extension
303
+
304
+ ### Custom detector
305
+
306
+ ```typescript
307
+ import { RaspAgent, Detector, NormalizedRequest, DetectionResult } from "@queno/agent-node";
308
+
309
+ const myDetector: Detector = {
310
+ name: "my-detector",
311
+ detect(req: NormalizedRequest): DetectionResult | null {
312
+ if (req.path.startsWith("/internal")) {
313
+ return {
314
+ eventType: "unauthorized_internal_access",
315
+ severity: "high",
316
+ detectorName: "my-detector",
317
+ matchedValue: req.path,
318
+ matchedField: "path",
319
+ };
320
+ }
321
+ return null;
322
+ },
323
+ };
324
+
325
+ const agent = new RaspAgent(
326
+ { apiKey, projectId, agentId },
327
+ { extraDetectors: [myDetector] },
328
+ );
329
+ ```
330
+
331
+ ### Prisma instrumentation (BOLA DB correlation)
332
+
333
+ ```typescript
334
+ import { PrismaClient } from "@prisma/client";
335
+ import { instrumentPrismaClient } from "@queno/agent-node";
336
+
337
+ const prisma = new PrismaClient({
338
+ log: [{ emit: "event", level: "query" }], // required
339
+ });
340
+ instrumentPrismaClient(prisma);
341
+
342
+ const agent = new RaspAgent({ ..., instrumentDb: true });
343
+ ```
344
+
345
+ Also supports `pg`, `mysql2`, `sequelize`, `mongoose`, and `knex` via `instrumentDatabaseDrivers()` (called automatically when `instrumentDb: true`).
346
+
347
+ ---
348
+
349
+ ## Self-protection
350
+
351
+ When `selfProtect: true`:
352
+
353
+ - **Anti-debug** - warns if `--inspect`/`--debug` args or a V8 inspector is detected
354
+ - **Hook integrity** - polls every 30s to detect tampering of installed DB driver hooks
355
+ - **SecureStore** - always active regardless of this flag: `apiKey` and `hmacSecret` are encrypted in-process
356
+
357
+ ---
358
+
359
+ ## API discovery
360
+
361
+ The agent passively observes HTTP endpoints at runtime and normalizes routes (`:id` substitution for numeric, UUID, and CUID-like segments). Observations are flushed every 60 seconds (configurable) to `POST /v1/discovery` on the collector.
362
+
363
+ Discover shadow APIs, zombie endpoints (no traffic in 30 days), and get an auth coverage heatmap in the dashboard. Export your live API inventory as an OpenAPI spec.
364
+
365
+ ---
366
+
367
+ ## Development warning
368
+
369
+ > **⚠ The default `policyPublicKey` is a development key** bundled in `src/config.ts`. It works out of the box with the RASP platform seed data. **Before production deployment, override it** with your actual Ed25519 public key via `RaspConfig.policyPublicKey` or `RASP_POLICY_PUBLIC_KEY`.
370
+
371
+ ---
372
+
373
+ ## Architecture overview
374
+
375
+ ```
376
+ Host application
377
+ └── RASP middleware (Express / Fastify / NestJS)
378
+ ├── inspect(req)
379
+ │ ├── EndpointObserver.observe() ← passive API discovery
380
+ │ ├── Detectors[0..N].detect() ← first-match, sync
381
+ │ └── handleDetection() [async]
382
+ │ ├── RedactionEngine.redact() ← fail-closed on error
383
+ │ ├── AuditLog.write() ← local JSONL, never sent
384
+ │ └── EventBuffer.enqueue() ← batch flush → collector
385
+ └── endRequest(outcome)
386
+ ├── EndpointObserver.observeOutcome()
387
+ └── correlateBola() ← DB query correlation (opt-in)
388
+
389
+ EventBuffer → POST /v1/events → rasp-collector → PostgreSQL
390
+ Heartbeat → POST /v1/heartbeat (kill-switch, policy version, upgrade)
391
+ ← GET /v1/policy (Ed25519 signed, verified locally)
392
+ Discovery → POST /v1/discovery (batched, 60s)
393
+ ```
394
+
395
+ ---
396
+
397
+ ## Testing
398
+
399
+ ```bash
400
+ pnpm test # vitest run
401
+ pnpm test:watch # vitest watch
402
+ pnpm typecheck # tsc --noEmit
403
+ ```
404
+
405
+ Test coverage areas: redaction engine, redaction patterns (email/card/SIN/IP), policy rejection, canonical bytes, custom rules, self-protect, hook integrity.
406
+
407
+ See `examples/banking-api/` for a full end-to-end test harness (Express app with intentionally vulnerable routes).
408
+
409
+ ---
410
+
411
+ ## Further reading
412
+
413
+ - [`AGENTS.md`](./AGENTS.md) - architecture rules and security invariants for contributors
414
+ - [`examples/banking-api/README.md`](./examples/banking-api/README.md) - local test lab with attack payloads
415
+ - [Technical documentation](../rasp/docs/dossier-technique.html) - full architecture doc, integration guide, design decisions, and recommendations (French)
416
+
417
+ ---
418
+
419
+ ## License
420
+
421
+ MIT
@@ -0,0 +1,222 @@
1
+ /**
2
+ * {@link RaspAgent} - the entry point of the RASP runtime.
3
+ *
4
+ * Wires together five subsystems:
5
+ * 1. Detectors - pattern matchers run on every incoming request.
6
+ * 2. Redaction engine - strips secrets/PII before anything leaves the process.
7
+ * 3. Audit log - local JSONL trace of every redaction action.
8
+ * 4. Event buffer - batches sanitised events to the collector.
9
+ * 5. Heartbeat - liveness signal + kill-switch / policy delivery channel.
10
+ *
11
+ * Invariants enforced here:
12
+ * - {@link RaspAgent.inspect} never throws (fail-open).
13
+ * - A detection is **never** enqueued before passing through the redaction
14
+ * engine; if redaction fails the event is dropped and audit-logged.
15
+ * - When the kill switch is received, inspection is disabled and the buffer
16
+ * is drained.
17
+ */
18
+ import { type ValidatedRaspConfig } from "./config.js";
19
+ import type { RaspConfig, NormalizedRequest, DetectionResult, AgentMode } from "./types.js";
20
+ import { type Detector } from "./detectors/index.js";
21
+ import { type RequestContext } from "./runtime-context.js";
22
+ import type { RequestOutcome } from "./types.js";
23
+ export declare class RaspAgent {
24
+ /** Fully validated configuration with defaults applied. */
25
+ readonly cfg: ValidatedRaspConfig;
26
+ private redaction;
27
+ private readonly auditLog;
28
+ private readonly client;
29
+ private readonly buffer;
30
+ private readonly heartbeat;
31
+ /** Built-in signature detectors. */
32
+ private readonly baseDetectors;
33
+ /** User-supplied detectors passed to the constructor. */
34
+ private readonly extraDetectors;
35
+ /** Active detector chain, rebuilt when custom rules arrive via policy. */
36
+ private detectors;
37
+ private readonly observer;
38
+ private readonly discoveryBuffer;
39
+ /** Verifies and tracks signed policies distributed by the control plane. */
40
+ private readonly policyManager;
41
+ /** Data residency directives applied in {@link handleDetection}. */
42
+ private dataResidency;
43
+ /** Version the control plane wants this agent to run (canary cohort / pin). */
44
+ private targetVersion;
45
+ /** True when an upgrade is available for this agent. */
46
+ private upgradePending;
47
+ /** Set to true after a kill-switch heartbeat - short-circuits {@link inspect}. */
48
+ private killed;
49
+ /** Stops the self-protection timer (Addendum E.7); null when disabled. */
50
+ private stopSelfProtection;
51
+ /**
52
+ * Enforcement mode in effect. Initialised from `cfg.mode` and updated
53
+ * whenever the collector returns a different mode in a heartbeat response.
54
+ */
55
+ private currentMode;
56
+ /**
57
+ * Deduplication key for `policy_rejected` telemetry: `"<version>:<reason>"`.
58
+ * Prevents spamming the collector on every heartbeat cycle when the same
59
+ * policy is repeatedly rejected (e.g. a persistent trust-anchor mismatch).
60
+ */
61
+ private lastRejectedPolicyKey;
62
+ /**
63
+ * Build a new agent.
64
+ *
65
+ * The constructor validates the config (throws on bad input), instantiates
66
+ * the subsystems and registers the default detectors. The heartbeat loop is
67
+ * **not** started until {@link start} is called.
68
+ *
69
+ * @param rawConfig - User-supplied configuration. Validated via
70
+ * {@link validateConfig}; throws if required fields are missing.
71
+ * @param extraDetectors - Optional custom detectors appended after the
72
+ * built-in set. They run in declaration order and the first non-null
73
+ * detection wins.
74
+ */
75
+ constructor(rawConfig: RaspConfig, extraDetectors?: Detector[]);
76
+ /**
77
+ * Start the heartbeat loop. Idempotent.
78
+ *
79
+ * Call once during application bootstrap, after the framework middleware
80
+ * has been registered. The buffer flush timer is already armed by the
81
+ * constructor.
82
+ */
83
+ start(): void;
84
+ /**
85
+ * Begin a request's runtime context for DB correlation. Called by the
86
+ * framework integration at the start of the request, before the handler runs,
87
+ * so DB queries issued during the request are attributed to it.
88
+ *
89
+ * @returns The bound context, or `null` when DB instrumentation is disabled.
90
+ */
91
+ beginRequest(req: NormalizedRequest): RequestContext | null;
92
+ /**
93
+ * Finalise a request: record its outcome for traffic profiling and, when a
94
+ * DB context is present, run BOLA-via-DB correlation and report any finding.
95
+ */
96
+ endRequest(ctx: RequestContext | null, req: NormalizedRequest, outcome: RequestOutcome): void;
97
+ /**
98
+ * The enforcement mode currently in effect. Reflects remote mode changes
99
+ * (heartbeat) and applied policies, so integrations should consult this
100
+ * rather than the boot config.
101
+ */
102
+ get mode(): AgentMode;
103
+ /** The policy version currently applied (0 if none). */
104
+ get policyVersion(): number;
105
+ /**
106
+ * Version the control plane wants this agent to run, and whether an upgrade
107
+ * is pending. The Node agent is delivered as an npm package and cannot
108
+ * hot-swap its own binary, so the upgrade is surfaced (logged + queryable)
109
+ * for the host's deployment automation to act on, rather than self-applied.
110
+ */
111
+ get desiredVersion(): {
112
+ targetVersion: string | null;
113
+ upgradePending: boolean;
114
+ };
115
+ /**
116
+ * React to the target version advertised by the heartbeat. Records the target
117
+ * and logs an actionable message when an upgrade is available. Never throws.
118
+ */
119
+ private handleTargetVersion;
120
+ /**
121
+ * Graceful shutdown.
122
+ *
123
+ * Stops the heartbeat timer, drains the buffer (final flush) and closes
124
+ * the audit log file descriptor. Safe to await during process exit.
125
+ */
126
+ stop(): Promise<void>;
127
+ /**
128
+ * Run every detector against a normalised request.
129
+ *
130
+ * Detectors run in registration order; the first one returning a non-null
131
+ * result wins. Detector exceptions are swallowed so that a bug in one
132
+ * detector cannot take down the host application.
133
+ *
134
+ * @param req - Framework-agnostic request view.
135
+ * @returns The first {@link DetectionResult} found, or `null` when the
136
+ * request is clean (or when the kill switch is active).
137
+ *
138
+ * @remarks Side-effects (redaction, audit log, buffering) happen in the
139
+ * background via {@link handleDetection}; this method itself returns
140
+ * synchronously.
141
+ */
142
+ /**
143
+ * Record the response-phase outcome of a request for API-discovery traffic
144
+ * profiling (status code, latency, confirmed auth middleware execution).
145
+ * Called by the framework integration's response hook. Never throws.
146
+ */
147
+ observeOutcome(req: NormalizedRequest, outcome: import("./types.js").RequestOutcome): void;
148
+ inspect(req: NormalizedRequest): DetectionResult | null;
149
+ /**
150
+ * Build a raw event payload, redact it, audit-log locally and enqueue it.
151
+ *
152
+ * Control flow:
153
+ * 1. Assemble the raw {@link EventPayload} from config + detection + request.
154
+ * 2. Run it through the {@link RedactionEngine}. On failure → drop and
155
+ * audit-log with `dropped: true`.
156
+ * 3. If any field was redacted → write a non-dropped audit-log line.
157
+ * 4. Stamp `auditLoggedLocally` into the event metadata and enqueue.
158
+ */
159
+ private handleDetection;
160
+ /**
161
+ * Apply data residency directives (Addendum B.3) before an event is queued
162
+ * for transmission.
163
+ *
164
+ * @returns The (possibly trimmed) event to send, or `null` if the event must
165
+ * stay local / be skipped. The local audit log has already been written by
166
+ * the caller, so dropping here still leaves a verifiable local record.
167
+ */
168
+ private applyDataResidency;
169
+ /**
170
+ * Handle a kill-switch heartbeat response.
171
+ *
172
+ * Disables inspection, drains the buffer and closes the audit log. The
173
+ * heartbeat scheduler has already stopped itself by the time this is
174
+ * called.
175
+ */
176
+ private handleKillSwitch;
177
+ /**
178
+ * Called when the kill switch transitions from active back to inactive.
179
+ * Re-enables inspection and restarts self-protection if configured.
180
+ */
181
+ private handleRecover;
182
+ /**
183
+ * React to a policy-version change signalled by the heartbeat.
184
+ *
185
+ * Fetches the latest signed policy, verifies its Ed25519 signature against
186
+ * the pinned trust anchor, and applies it. An untrusted, malformed or stale
187
+ * policy is ignored and the agent keeps its current configuration
188
+ * (self-rollback of policy - Addendum D.4 / E.4.1). Never throws.
189
+ *
190
+ * Rejections are surfaced via a `console.warn` and a `policy_rejected`
191
+ * telemetry event so operators can diagnose trust-anchor mismatches without
192
+ * reading raw application logs.
193
+ */
194
+ private handlePolicyChange;
195
+ /**
196
+ * Emit a single `policy_rejected` telemetry event so the control plane can
197
+ * surface trust-anchor or signature mismatches in the dashboard.
198
+ *
199
+ * The event is deduplicated by `version:reason` so a persistent rejection
200
+ * (e.g. a misconfigured trust anchor that the operator has not yet fixed)
201
+ * does not flood the collector on every heartbeat cycle.
202
+ *
203
+ * The payload passes through the redaction engine (Addendum B.2 / AGENTS.md
204
+ * rule: "All telemetry must pass through the redaction engine before
205
+ * buffering") and the data-residency filter before being enqueued.
206
+ */
207
+ private enqueuePolicyRejected;
208
+ /**
209
+ * Apply a verified policy to the live agent: enforcement mode, customer
210
+ * detection rules, redaction configuration and data residency directives.
211
+ */
212
+ private applyPolicy;
213
+ /** Rebuild the active detector chain from base + custom rules + extras. */
214
+ private rebuildDetectors;
215
+ /**
216
+ * Reconfigure the redaction engine from a policy. The engine always keeps its
217
+ * built-in protections; the policy can only add field-name patterns and tune
218
+ * value-based redaction / IP handling.
219
+ */
220
+ private applyRedactionConfig;
221
+ }
222
+ //# sourceMappingURL=agent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAiC,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACtF,OAAO,KAAK,EACV,UAAU,EACV,iBAAiB,EAEjB,eAAe,EACf,SAAS,EACV,MAAM,YAAY,CAAC;AAMpB,OAAO,EAA0B,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAc7E,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACzE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,qBAAa,SAAS;IACpB,2DAA2D;IAC3D,QAAQ,CAAC,GAAG,EAAE,mBAAmB,CAAC;IAClC,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkB;IAC3C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqB;IAC/C,oCAAoC;IACpC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAa;IAC3C,yDAAyD;IACzD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAa;IAC5C,0EAA0E;IAC1E,OAAO,CAAC,SAAS,CAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmB;IAC5C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,4EAA4E;IAC5E,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,oEAAoE;IACpE,OAAO,CAAC,aAAa,CAAoC;IACzD,+EAA+E;IAC/E,OAAO,CAAC,aAAa,CAAuB;IAC5C,wDAAwD;IACxD,OAAO,CAAC,cAAc,CAAS;IAC/B,kFAAkF;IAClF,OAAO,CAAC,MAAM,CAAS;IACvB,0EAA0E;IAC1E,OAAO,CAAC,kBAAkB,CAA6B;IACvD;;;OAGG;IACH,OAAO,CAAC,WAAW,CAAY;IAC/B;;;;OAIG;IACH,OAAO,CAAC,qBAAqB,CAAuB;IAEpD;;;;;;;;;;;;OAYG;gBACS,SAAS,EAAE,UAAU,EAAE,cAAc,GAAE,QAAQ,EAAO;IA2DlE;;;;;;OAMG;IACH,KAAK,IAAI,IAAI;IAcb;;;;;;OAMG;IACH,YAAY,CAAC,GAAG,EAAE,iBAAiB,GAAG,cAAc,GAAG,IAAI;IAmB3D;;;OAGG;IACH,UAAU,CACR,GAAG,EAAE,cAAc,GAAG,IAAI,EAC1B,GAAG,EAAE,iBAAiB,EACtB,OAAO,EAAE,cAAc,GACtB,IAAI;IAeP;;;;OAIG;IACH,IAAI,IAAI,IAAI,SAAS,CAEpB;IAED,wDAAwD;IACxD,IAAI,aAAa,IAAI,MAAM,CAE1B;IAED;;;;;OAKG;IACH,IAAI,cAAc,IAAI;QAAE,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,cAAc,EAAE,OAAO,CAAA;KAAE,CAE9E;IAED;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IAmB3B;;;;;OAKG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ3B;;;;;;;;;;;;;;OAcG;IACH;;;;OAIG;IACH,cAAc,CAAC,GAAG,EAAE,iBAAiB,EAAE,OAAO,EAAE,OAAO,YAAY,EAAE,cAAc,GAAG,IAAI;IAS1F,OAAO,CAAC,GAAG,EAAE,iBAAiB,GAAG,eAAe,GAAG,IAAI;IAsBvD;;;;;;;;;OASG;YACW,eAAe;IAoE7B;;;;;;;OAOG;IACH,OAAO,CAAC,kBAAkB;IA8B1B;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB;IAQxB;;;OAGG;IACH,OAAO,CAAC,aAAa;IAUrB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,kBAAkB;IAiD1B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,qBAAqB;IAwC7B;;;OAGG;IACH,OAAO,CAAC,WAAW;IAgBnB,2EAA2E;IAC3E,OAAO,CAAC,gBAAgB;IAUxB;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;CAG7B"}