@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.
- package/README.md +421 -0
- package/dist/agent.d.ts +222 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +591 -0
- package/dist/agent.js.map +1 -0
- package/dist/api-discovery/discovery-buffer.d.ts +27 -0
- package/dist/api-discovery/discovery-buffer.d.ts.map +1 -0
- package/dist/api-discovery/discovery-buffer.js +50 -0
- package/dist/api-discovery/discovery-buffer.js.map +1 -0
- package/dist/api-discovery/endpoint-observer.d.ts +25 -0
- package/dist/api-discovery/endpoint-observer.d.ts.map +1 -0
- package/dist/api-discovery/endpoint-observer.js +127 -0
- package/dist/api-discovery/endpoint-observer.js.map +1 -0
- package/dist/api-discovery/route-normalizer.d.ts +15 -0
- package/dist/api-discovery/route-normalizer.d.ts.map +1 -0
- package/dist/api-discovery/route-normalizer.js +34 -0
- package/dist/api-discovery/route-normalizer.js.map +1 -0
- package/dist/config.d.ts +100 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +101 -0
- package/dist/config.js.map +1 -0
- package/dist/db-hooks/correlate.d.ts +19 -0
- package/dist/db-hooks/correlate.d.ts.map +1 -0
- package/dist/db-hooks/correlate.js +45 -0
- package/dist/db-hooks/correlate.js.map +1 -0
- package/dist/db-hooks/instrument.d.ts +27 -0
- package/dist/db-hooks/instrument.d.ts.map +1 -0
- package/dist/db-hooks/instrument.js +194 -0
- package/dist/db-hooks/instrument.js.map +1 -0
- package/dist/detectors/base.d.ts +61 -0
- package/dist/detectors/base.d.ts.map +1 -0
- package/dist/detectors/base.js +57 -0
- package/dist/detectors/base.js.map +1 -0
- package/dist/detectors/bola.d.ts +60 -0
- package/dist/detectors/bola.d.ts.map +1 -0
- package/dist/detectors/bola.js +108 -0
- package/dist/detectors/bola.js.map +1 -0
- package/dist/detectors/command-injection.d.ts +22 -0
- package/dist/detectors/command-injection.d.ts.map +1 -0
- package/dist/detectors/command-injection.js +41 -0
- package/dist/detectors/command-injection.js.map +1 -0
- package/dist/detectors/custom-rule.d.ts +24 -0
- package/dist/detectors/custom-rule.d.ts.map +1 -0
- package/dist/detectors/custom-rule.js +65 -0
- package/dist/detectors/custom-rule.js.map +1 -0
- package/dist/detectors/index.d.ts +17 -0
- package/dist/detectors/index.d.ts.map +1 -0
- package/dist/detectors/index.js +31 -0
- package/dist/detectors/index.js.map +1 -0
- package/dist/detectors/nosql-injection.d.ts +23 -0
- package/dist/detectors/nosql-injection.d.ts.map +1 -0
- package/dist/detectors/nosql-injection.js +54 -0
- package/dist/detectors/nosql-injection.js.map +1 -0
- package/dist/detectors/path-traversal.d.ts +21 -0
- package/dist/detectors/path-traversal.d.ts.map +1 -0
- package/dist/detectors/path-traversal.js +54 -0
- package/dist/detectors/path-traversal.js.map +1 -0
- package/dist/detectors/prototype-pollution.d.ts +23 -0
- package/dist/detectors/prototype-pollution.d.ts.map +1 -0
- package/dist/detectors/prototype-pollution.js +50 -0
- package/dist/detectors/prototype-pollution.js.map +1 -0
- package/dist/detectors/sql-injection.d.ts +22 -0
- package/dist/detectors/sql-injection.d.ts.map +1 -0
- package/dist/detectors/sql-injection.js +42 -0
- package/dist/detectors/sql-injection.js.map +1 -0
- package/dist/detectors/ssrf.d.ts +26 -0
- package/dist/detectors/ssrf.d.ts.map +1 -0
- package/dist/detectors/ssrf.js +37 -0
- package/dist/detectors/ssrf.js.map +1 -0
- package/dist/detectors/suspicious-headers.d.ts +25 -0
- package/dist/detectors/suspicious-headers.d.ts.map +1 -0
- package/dist/detectors/suspicious-headers.js +87 -0
- package/dist/detectors/suspicious-headers.js.map +1 -0
- package/dist/detectors/template-injection.d.ts +27 -0
- package/dist/detectors/template-injection.d.ts.map +1 -0
- package/dist/detectors/template-injection.js +35 -0
- package/dist/detectors/template-injection.js.map +1 -0
- package/dist/detectors/xss.d.ts +22 -0
- package/dist/detectors/xss.d.ts.map +1 -0
- package/dist/detectors/xss.js +38 -0
- package/dist/detectors/xss.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/express.d.ts +39 -0
- package/dist/integrations/express.d.ts.map +1 -0
- package/dist/integrations/express.js +62 -0
- package/dist/integrations/express.js.map +1 -0
- package/dist/integrations/fastify.d.ts +33 -0
- package/dist/integrations/fastify.d.ts.map +1 -0
- package/dist/integrations/fastify.js +63 -0
- package/dist/integrations/fastify.js.map +1 -0
- package/dist/integrations/nestjs.d.ts +40 -0
- package/dist/integrations/nestjs.d.ts.map +1 -0
- package/dist/integrations/nestjs.js +58 -0
- package/dist/integrations/nestjs.js.map +1 -0
- package/dist/policy/canonical.d.ts +23 -0
- package/dist/policy/canonical.d.ts.map +1 -0
- package/dist/policy/canonical.js +40 -0
- package/dist/policy/canonical.js.map +1 -0
- package/dist/policy/policy-manager.d.ts +43 -0
- package/dist/policy/policy-manager.d.ts.map +1 -0
- package/dist/policy/policy-manager.js +89 -0
- package/dist/policy/policy-manager.js.map +1 -0
- package/dist/policy/types.d.ts +70 -0
- package/dist/policy/types.d.ts.map +1 -0
- package/dist/policy/types.js +2 -0
- package/dist/policy/types.js.map +1 -0
- package/dist/policy/verify.d.ts +11 -0
- package/dist/policy/verify.d.ts.map +1 -0
- package/dist/policy/verify.js +61 -0
- package/dist/policy/verify.js.map +1 -0
- package/dist/redaction/audit-log.d.ts +40 -0
- package/dist/redaction/audit-log.d.ts.map +1 -0
- package/dist/redaction/audit-log.js +110 -0
- package/dist/redaction/audit-log.js.map +1 -0
- package/dist/redaction/engine.d.ts +50 -0
- package/dist/redaction/engine.d.ts.map +1 -0
- package/dist/redaction/engine.js +143 -0
- package/dist/redaction/engine.js.map +1 -0
- package/dist/redaction/patterns.d.ts +24 -0
- package/dist/redaction/patterns.d.ts.map +1 -0
- package/dist/redaction/patterns.js +142 -0
- package/dist/redaction/patterns.js.map +1 -0
- package/dist/runtime-context.d.ts +33 -0
- package/dist/runtime-context.d.ts.map +1 -0
- package/dist/runtime-context.js +46 -0
- package/dist/runtime-context.js.map +1 -0
- package/dist/self-protect.d.ts +34 -0
- package/dist/self-protect.d.ts.map +1 -0
- package/dist/self-protect.js +134 -0
- package/dist/self-protect.js.map +1 -0
- package/dist/transport/buffer.d.ts +52 -0
- package/dist/transport/buffer.d.ts.map +1 -0
- package/dist/transport/buffer.js +57 -0
- package/dist/transport/buffer.js.map +1 -0
- package/dist/transport/client.d.ts +77 -0
- package/dist/transport/client.d.ts.map +1 -0
- package/dist/transport/client.js +178 -0
- package/dist/transport/client.js.map +1 -0
- package/dist/transport/heartbeat.d.ts +86 -0
- package/dist/transport/heartbeat.d.ts.map +1 -0
- package/dist/transport/heartbeat.js +110 -0
- package/dist/transport/heartbeat.js.map +1 -0
- package/dist/transport/secure-request.d.ts +30 -0
- package/dist/transport/secure-request.d.ts.map +1 -0
- package/dist/transport/secure-request.js +95 -0
- package/dist/transport/secure-request.js.map +1 -0
- package/dist/types.d.ts +311 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export class EventBuffer {
|
|
2
|
+
queue = [];
|
|
3
|
+
client;
|
|
4
|
+
maxSize;
|
|
5
|
+
timer = null;
|
|
6
|
+
constructor(client, cfg) {
|
|
7
|
+
this.client = client;
|
|
8
|
+
this.maxSize = cfg.maxSize;
|
|
9
|
+
this.timer = setInterval(() => {
|
|
10
|
+
this.flush().catch(() => {
|
|
11
|
+
// Fail-open - transport errors must not propagate.
|
|
12
|
+
});
|
|
13
|
+
}, cfg.flushIntervalMs);
|
|
14
|
+
if (this.timer.unref) {
|
|
15
|
+
this.timer.unref();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Append an event. Triggers an immediate flush once the queue reaches
|
|
20
|
+
* `maxSize`.
|
|
21
|
+
*
|
|
22
|
+
* The event is expected to have already passed through the redaction
|
|
23
|
+
* engine - the buffer does not sanitise.
|
|
24
|
+
*/
|
|
25
|
+
enqueue(event) {
|
|
26
|
+
this.queue.push(event);
|
|
27
|
+
if (this.queue.length >= this.maxSize) {
|
|
28
|
+
this.flush().catch(() => { });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Atomically drain the queue and fire all sends in parallel.
|
|
33
|
+
*
|
|
34
|
+
* Uses `Promise.allSettled` so a single failed send cannot block the
|
|
35
|
+
* others. Per-event errors are caught and ignored (the event is lost).
|
|
36
|
+
*/
|
|
37
|
+
async flush() {
|
|
38
|
+
if (this.queue.length === 0)
|
|
39
|
+
return;
|
|
40
|
+
const batch = this.queue.splice(0, this.queue.length);
|
|
41
|
+
await Promise.allSettled(batch.map((event) => this.client.sendEvent(event).catch(() => {
|
|
42
|
+
// Per-event failure - the event is dropped.
|
|
43
|
+
})));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Cancel the timer and perform a final drain. Awaited during agent
|
|
47
|
+
* shutdown.
|
|
48
|
+
*/
|
|
49
|
+
async stop() {
|
|
50
|
+
if (this.timer) {
|
|
51
|
+
clearInterval(this.timer);
|
|
52
|
+
this.timer = null;
|
|
53
|
+
}
|
|
54
|
+
await this.flush();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=buffer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buffer.js","sourceRoot":"","sources":["../../src/transport/buffer.ts"],"names":[],"mappings":"AA0BA,MAAM,OAAO,WAAW;IACL,KAAK,GAAmB,EAAE,CAAC;IAC3B,MAAM,CAAkB;IACxB,OAAO,CAAS;IACzB,KAAK,GAA0C,IAAI,CAAC;IAE5D,YAAY,MAAuB,EAAE,GAAiB;QACpD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;QAE3B,IAAI,CAAC,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;YAC5B,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE;gBACtB,mDAAmD;YACrD,CAAC,CAAC,CAAC;QACL,CAAC,EAAE,GAAG,CAAC,eAAe,CAAC,CAAC;QAExB,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;YACrB,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACrB,CAAC;IACH,CAAC;IAED;;;;;;OAMG;IACH,OAAO,CAAC,KAAmB;QACzB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAEvB,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACtC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC/B,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,KAAK;QACT,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAEpC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAEtD,MAAM,OAAO,CAAC,UAAU,CACtB,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAClB,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;YACtC,4CAA4C;QAC9C,CAAC,CAAC,CACH,CACF,CAAC;IACJ,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,IAAI;QACR,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC1B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QACpB,CAAC;QACD,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;CACF"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { EventPayload, HeartbeatPayload, HeartbeatResponse, DiscoveryPayload } from "../types.js";
|
|
2
|
+
import type { DistributedPolicy } from "../policy/types.js";
|
|
3
|
+
import { type TlsOptions } from "./secure-request.js";
|
|
4
|
+
export interface TransportConfig {
|
|
5
|
+
/** Base URL of the collector (no trailing slash required). */
|
|
6
|
+
collectorUrl: string;
|
|
7
|
+
/** Bearer API key sent with every request. */
|
|
8
|
+
apiKey: string;
|
|
9
|
+
/** Per-request timeout in ms (also enforced via `AbortController`). */
|
|
10
|
+
timeoutMs: number;
|
|
11
|
+
/**
|
|
12
|
+
* Optional per-agent HMAC secret. When set, every request body is signed and
|
|
13
|
+
* the digest is sent as `X-RASP-Signature: sha256=<hex>` (Addendum E.5).
|
|
14
|
+
*/
|
|
15
|
+
hmacSecret?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Optional TLS options enabling certificate pinning + mutual TLS. When set,
|
|
18
|
+
* requests use a pinned HTTPS transport instead of `fetch` (Addendum E.4.2).
|
|
19
|
+
*/
|
|
20
|
+
tls?: TlsOptions;
|
|
21
|
+
}
|
|
22
|
+
export declare class TransportClient {
|
|
23
|
+
private readonly baseUrl;
|
|
24
|
+
private readonly timeoutMs;
|
|
25
|
+
private readonly tls?;
|
|
26
|
+
/**
|
|
27
|
+
* Secrets (API key, HMAC secret) are held encrypted in memory rather than as
|
|
28
|
+
* plaintext fields, so a heap dump or accidental log of the client does not
|
|
29
|
+
* leak them (Addendum E.7).
|
|
30
|
+
*/
|
|
31
|
+
private readonly secrets;
|
|
32
|
+
private readonly hasHmac;
|
|
33
|
+
constructor(cfg: TransportConfig);
|
|
34
|
+
/** Build request headers, decrypting the API key on demand. */
|
|
35
|
+
private buildHeaders;
|
|
36
|
+
/** Compute the `X-RASP-Signature` value for a serialised body, if signing. */
|
|
37
|
+
private signBody;
|
|
38
|
+
/**
|
|
39
|
+
* POST a sanitised event to `/v1/events`.
|
|
40
|
+
*
|
|
41
|
+
* @throws On non-2xx responses or network errors. The buffer treats this
|
|
42
|
+
* as a per-event failure and drops the event (fail-open).
|
|
43
|
+
*/
|
|
44
|
+
sendEvent(payload: EventPayload): Promise<void>;
|
|
45
|
+
sendDiscovery(payload: DiscoveryPayload): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Fetch the latest signed policy from `GET /v1/policy`.
|
|
48
|
+
*
|
|
49
|
+
* Returns the parsed {@link DistributedPolicy} or `null` on any failure
|
|
50
|
+
* (network, 404 when no policy is published, etc.). Errors are swallowed so a
|
|
51
|
+
* policy fetch can never crash the host application.
|
|
52
|
+
*
|
|
53
|
+
* @param agentId - Used by the collector to resolve the agent's channel.
|
|
54
|
+
* @param channel - Optional explicit channel override.
|
|
55
|
+
*/
|
|
56
|
+
fetchPolicy(agentId: string, channel?: string): Promise<DistributedPolicy | null>;
|
|
57
|
+
/**
|
|
58
|
+
* POST a heartbeat to `/v1/heartbeat`.
|
|
59
|
+
*
|
|
60
|
+
* @returns The parsed {@link HeartbeatResponse}, or `null` if the request
|
|
61
|
+
* failed for any reason. Errors are intentionally suppressed - the
|
|
62
|
+
* heartbeat must never crash the host.
|
|
63
|
+
*/
|
|
64
|
+
sendHeartbeat(payload: HeartbeatPayload): Promise<HeartbeatResponse | null>;
|
|
65
|
+
/**
|
|
66
|
+
* Shared POST helper.
|
|
67
|
+
*
|
|
68
|
+
* Sets up an `AbortController` that fires after `timeoutMs`, sends the
|
|
69
|
+
* JSON body and rejects on a non-2xx status.
|
|
70
|
+
*/
|
|
71
|
+
private post;
|
|
72
|
+
/**
|
|
73
|
+
* Shared GET helper. Same timeout budget and non-2xx handling as POST.
|
|
74
|
+
*/
|
|
75
|
+
private get;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/transport/client.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACvG,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAGrE,MAAM,WAAW,eAAe;IAC9B,8DAA8D;IAC9D,YAAY,EAAE,MAAM,CAAC;IACrB,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,GAAG,CAAC,EAAE,UAAU,CAAC;CAClB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAa;IAClC;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;gBAEtB,GAAG,EAAE,eAAe;IAShC,+DAA+D;IAC/D,OAAO,CAAC,YAAY;IAQpB,8EAA8E;IAC9E,OAAO,CAAC,QAAQ;IAQhB;;;;;OAKG;IACG,SAAS,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/C,aAAa,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7D;;;;;;;;;OASG;IACG,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAWvF;;;;;;OAMG;IACG,aAAa,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IASjF;;;;;OAKG;YACW,IAAI;IAyClB;;OAEG;YACW,GAAG;CAgClB"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin HTTP client used to talk to the collector.
|
|
3
|
+
*
|
|
4
|
+
* The agent only ever calls two endpoints - `POST /v1/events` and
|
|
5
|
+
* `POST /v1/heartbeat`. Both use `Authorization: Bearer <apiKey>` and a JSON
|
|
6
|
+
* body. A per-call `AbortController` enforces the `timeoutMs` budget.
|
|
7
|
+
*
|
|
8
|
+
* Failure semantics differ between the two methods:
|
|
9
|
+
* - {@link sendEvent} **throws** on transport / non-2xx errors so the
|
|
10
|
+
* buffer layer can decide whether to retry or drop.
|
|
11
|
+
* - {@link sendHeartbeat} **swallows** errors and returns `null` so the
|
|
12
|
+
* heartbeat loop never crashes the host application.
|
|
13
|
+
*/
|
|
14
|
+
import { createHmac } from "node:crypto";
|
|
15
|
+
import { secureRequest } from "./secure-request.js";
|
|
16
|
+
import { SecureStore } from "../self-protect.js";
|
|
17
|
+
export class TransportClient {
|
|
18
|
+
baseUrl;
|
|
19
|
+
timeoutMs;
|
|
20
|
+
tls;
|
|
21
|
+
/**
|
|
22
|
+
* Secrets (API key, HMAC secret) are held encrypted in memory rather than as
|
|
23
|
+
* plaintext fields, so a heap dump or accidental log of the client does not
|
|
24
|
+
* leak them (Addendum E.7).
|
|
25
|
+
*/
|
|
26
|
+
secrets = new SecureStore();
|
|
27
|
+
hasHmac;
|
|
28
|
+
constructor(cfg) {
|
|
29
|
+
this.baseUrl = cfg.collectorUrl.replace(/\/$/, "");
|
|
30
|
+
this.timeoutMs = cfg.timeoutMs;
|
|
31
|
+
this.tls = cfg.tls;
|
|
32
|
+
this.secrets.set("apiKey", cfg.apiKey);
|
|
33
|
+
this.hasHmac = Boolean(cfg.hmacSecret);
|
|
34
|
+
if (cfg.hmacSecret)
|
|
35
|
+
this.secrets.set("hmacSecret", cfg.hmacSecret);
|
|
36
|
+
}
|
|
37
|
+
/** Build request headers, decrypting the API key on demand. */
|
|
38
|
+
buildHeaders(extra) {
|
|
39
|
+
return {
|
|
40
|
+
"Content-Type": "application/json",
|
|
41
|
+
Authorization: `Bearer ${this.secrets.get("apiKey") ?? ""}`,
|
|
42
|
+
...extra,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Compute the `X-RASP-Signature` value for a serialised body, if signing. */
|
|
46
|
+
signBody(body) {
|
|
47
|
+
if (!this.hasHmac)
|
|
48
|
+
return null;
|
|
49
|
+
const secret = this.secrets.get("hmacSecret");
|
|
50
|
+
if (!secret)
|
|
51
|
+
return null;
|
|
52
|
+
const digest = createHmac("sha256", secret).update(body).digest("hex");
|
|
53
|
+
return `sha256=${digest}`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* POST a sanitised event to `/v1/events`.
|
|
57
|
+
*
|
|
58
|
+
* @throws On non-2xx responses or network errors. The buffer treats this
|
|
59
|
+
* as a per-event failure and drops the event (fail-open).
|
|
60
|
+
*/
|
|
61
|
+
async sendEvent(payload) {
|
|
62
|
+
await this.post("/v1/events", payload);
|
|
63
|
+
}
|
|
64
|
+
async sendDiscovery(payload) {
|
|
65
|
+
await this.post("/v1/discovery", payload);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Fetch the latest signed policy from `GET /v1/policy`.
|
|
69
|
+
*
|
|
70
|
+
* Returns the parsed {@link DistributedPolicy} or `null` on any failure
|
|
71
|
+
* (network, 404 when no policy is published, etc.). Errors are swallowed so a
|
|
72
|
+
* policy fetch can never crash the host application.
|
|
73
|
+
*
|
|
74
|
+
* @param agentId - Used by the collector to resolve the agent's channel.
|
|
75
|
+
* @param channel - Optional explicit channel override.
|
|
76
|
+
*/
|
|
77
|
+
async fetchPolicy(agentId, channel) {
|
|
78
|
+
try {
|
|
79
|
+
const qs = new URLSearchParams({ agentId });
|
|
80
|
+
if (channel)
|
|
81
|
+
qs.set("channel", channel);
|
|
82
|
+
const res = await this.get(`/v1/policy?${qs.toString()}`);
|
|
83
|
+
return res;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* POST a heartbeat to `/v1/heartbeat`.
|
|
91
|
+
*
|
|
92
|
+
* @returns The parsed {@link HeartbeatResponse}, or `null` if the request
|
|
93
|
+
* failed for any reason. Errors are intentionally suppressed - the
|
|
94
|
+
* heartbeat must never crash the host.
|
|
95
|
+
*/
|
|
96
|
+
async sendHeartbeat(payload) {
|
|
97
|
+
try {
|
|
98
|
+
const res = await this.post("/v1/heartbeat", payload);
|
|
99
|
+
return res;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Shared POST helper.
|
|
107
|
+
*
|
|
108
|
+
* Sets up an `AbortController` that fires after `timeoutMs`, sends the
|
|
109
|
+
* JSON body and rejects on a non-2xx status.
|
|
110
|
+
*/
|
|
111
|
+
async post(path, body) {
|
|
112
|
+
const controller = new AbortController();
|
|
113
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
114
|
+
try {
|
|
115
|
+
const serialized = JSON.stringify(body);
|
|
116
|
+
const signature = this.signBody(serialized);
|
|
117
|
+
const headers = this.buildHeaders(signature ? { "X-RASP-Signature": signature } : undefined);
|
|
118
|
+
// Pinned HTTPS / mutual-TLS path.
|
|
119
|
+
if (this.tls) {
|
|
120
|
+
return await secureRequest({
|
|
121
|
+
method: "POST",
|
|
122
|
+
url: `${this.baseUrl}${path}`,
|
|
123
|
+
headers,
|
|
124
|
+
body: serialized,
|
|
125
|
+
timeoutMs: this.timeoutMs,
|
|
126
|
+
tls: this.tls,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
130
|
+
method: "POST",
|
|
131
|
+
headers,
|
|
132
|
+
body: serialized,
|
|
133
|
+
signal: controller.signal,
|
|
134
|
+
});
|
|
135
|
+
if (!res.ok) {
|
|
136
|
+
const text = await res.text().catch(() => "");
|
|
137
|
+
throw new Error(`[rasp-transport] ${path} → HTTP ${res.status}: ${text}`);
|
|
138
|
+
}
|
|
139
|
+
return res.json();
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Shared GET helper. Same timeout budget and non-2xx handling as POST.
|
|
147
|
+
*/
|
|
148
|
+
async get(path) {
|
|
149
|
+
const controller = new AbortController();
|
|
150
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
151
|
+
try {
|
|
152
|
+
const headers = this.buildHeaders();
|
|
153
|
+
if (this.tls) {
|
|
154
|
+
return await secureRequest({
|
|
155
|
+
method: "GET",
|
|
156
|
+
url: `${this.baseUrl}${path}`,
|
|
157
|
+
headers,
|
|
158
|
+
timeoutMs: this.timeoutMs,
|
|
159
|
+
tls: this.tls,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
163
|
+
method: "GET",
|
|
164
|
+
headers,
|
|
165
|
+
signal: controller.signal,
|
|
166
|
+
});
|
|
167
|
+
if (!res.ok) {
|
|
168
|
+
const text = await res.text().catch(() => "");
|
|
169
|
+
throw new Error(`[rasp-transport] ${path} → HTTP ${res.status}: ${text}`);
|
|
170
|
+
}
|
|
171
|
+
return res.json();
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
clearTimeout(timer);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/transport/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heartbeat scheduler.
|
|
3
|
+
*
|
|
4
|
+
* Periodically sends an {@link HeartbeatPayload} to the collector. The
|
|
5
|
+
* heartbeat is the bidirectional control channel: the agent reports its
|
|
6
|
+
* health and mode, and the collector replies with the kill-switch flag and
|
|
7
|
+
* the current policy version.
|
|
8
|
+
*
|
|
9
|
+
* Behaviour:
|
|
10
|
+
* - {@link start} immediately fires one heartbeat, then arms an interval
|
|
11
|
+
* of `cfg.heartbeatIntervalMs`. The timer is `unref`'d so it doesn't
|
|
12
|
+
* keep the process alive.
|
|
13
|
+
* - On a `killSwitch: true` response, the scheduler stops itself and
|
|
14
|
+
* invokes `onKillSwitch` so the agent can drain its buffer and disable
|
|
15
|
+
* inspection.
|
|
16
|
+
* - On a `policyVersion` change, `onPolicyChange` is invoked once
|
|
17
|
+
* (reserved for future dynamic rule refresh).
|
|
18
|
+
* - All heartbeat failures are swallowed by {@link TransportClient.sendHeartbeat}
|
|
19
|
+
* so the loop never crashes.
|
|
20
|
+
*/
|
|
21
|
+
import type { AgentStatus, AgentMode } from "../types.js";
|
|
22
|
+
import type { TransportClient } from "./client.js";
|
|
23
|
+
import type { ValidatedRaspConfig } from "../config.js";
|
|
24
|
+
export type KillSwitchHandler = () => void;
|
|
25
|
+
export type RecoverHandler = () => void;
|
|
26
|
+
export type PolicyChangeHandler = (version: string) => void;
|
|
27
|
+
export type ModeChangeHandler = (mode: AgentMode) => void;
|
|
28
|
+
export interface TargetVersionInfo {
|
|
29
|
+
targetVersion: string | null;
|
|
30
|
+
upgradeAvailable: boolean;
|
|
31
|
+
changelog: string | null;
|
|
32
|
+
impact: string | null;
|
|
33
|
+
}
|
|
34
|
+
export type TargetVersionHandler = (info: TargetVersionInfo) => void;
|
|
35
|
+
export declare class HeartbeatScheduler {
|
|
36
|
+
private readonly client;
|
|
37
|
+
private readonly cfg;
|
|
38
|
+
private timer;
|
|
39
|
+
private status;
|
|
40
|
+
private readonly onKillSwitch;
|
|
41
|
+
private readonly onRecover?;
|
|
42
|
+
private readonly onPolicyChange;
|
|
43
|
+
private readonly onModeChange;
|
|
44
|
+
private readonly onTargetVersion?;
|
|
45
|
+
private readonly getMode;
|
|
46
|
+
private lastPolicyVersion;
|
|
47
|
+
private lastMode;
|
|
48
|
+
private lastTargetVersion;
|
|
49
|
+
/** Tracks whether the kill switch was active on the last heartbeat response. */
|
|
50
|
+
private killSwitchActive;
|
|
51
|
+
/**
|
|
52
|
+
* @param client - Transport used to deliver heartbeats.
|
|
53
|
+
* @param cfg - Validated config. Provides identity, mode and interval.
|
|
54
|
+
* @param handlers - Callbacks fired when the backend signals a kill
|
|
55
|
+
* switch, a policy change, or a mode change.
|
|
56
|
+
*/
|
|
57
|
+
constructor(client: TransportClient, cfg: ValidatedRaspConfig, handlers: {
|
|
58
|
+
onKillSwitch: KillSwitchHandler;
|
|
59
|
+
/** Called once when the kill switch transitions from active back to inactive. */
|
|
60
|
+
onRecover?: RecoverHandler;
|
|
61
|
+
onPolicyChange: PolicyChangeHandler;
|
|
62
|
+
onModeChange: ModeChangeHandler;
|
|
63
|
+
onTargetVersion?: TargetVersionHandler;
|
|
64
|
+
/** Returns the agent's current enforcement mode so the heartbeat payload stays accurate. */
|
|
65
|
+
getMode: () => AgentMode;
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* Send an immediate heartbeat and arm the periodic loop. Idempotent -
|
|
69
|
+
* calling `start` twice has no extra effect.
|
|
70
|
+
*/
|
|
71
|
+
start(): void;
|
|
72
|
+
/** Cancel the periodic loop. Safe to call multiple times. */
|
|
73
|
+
stop(): void;
|
|
74
|
+
/**
|
|
75
|
+
* Override the self-reported status sent on the next beat. Used by the
|
|
76
|
+
* agent when subsystems degrade.
|
|
77
|
+
*/
|
|
78
|
+
setStatus(status: AgentStatus): void;
|
|
79
|
+
/**
|
|
80
|
+
* Send one heartbeat, then react to the response:
|
|
81
|
+
* - `killSwitch` → stop the loop and fire {@link onKillSwitch}.
|
|
82
|
+
* - `policyVersion` change → cache it and fire {@link onPolicyChange}.
|
|
83
|
+
*/
|
|
84
|
+
private beat;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=heartbeat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heartbeat.d.ts","sourceRoot":"","sources":["../../src/transport/heartbeat.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAoB,WAAW,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAExD,MAAM,MAAM,iBAAiB,GAAG,MAAM,IAAI,CAAC;AAC3C,MAAM,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC;AACxC,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AAC5D,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;AAC1D,MAAM,WAAW,iBAAiB;IAChC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AACD,MAAM,MAAM,oBAAoB,GAAG,CAAC,IAAI,EAAE,iBAAiB,KAAK,IAAI,CAAC;AAErE,qBAAa,kBAAkB;IAsB3B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAtBtB,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAoB;IACjD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAiB;IAC5C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAsB;IACrD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAoB;IACjD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAuB;IACxD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkB;IAC1C,OAAO,CAAC,iBAAiB,CAAM;IAC/B,OAAO,CAAC,QAAQ,CAAsB;IACtC,OAAO,CAAC,iBAAiB,CAAwC;IACjE,gFAAgF;IAChF,OAAO,CAAC,gBAAgB,CAAS;IAEjC;;;;;OAKG;gBAEgB,MAAM,EAAE,eAAe,EACvB,GAAG,EAAE,mBAAmB,EACzC,QAAQ,EAAE;QACR,YAAY,EAAE,iBAAiB,CAAC;QAChC,iFAAiF;QACjF,SAAS,CAAC,EAAE,cAAc,CAAC;QAC3B,cAAc,EAAE,mBAAmB,CAAC;QACpC,YAAY,EAAE,iBAAiB,CAAC;QAChC,eAAe,CAAC,EAAE,oBAAoB,CAAC;QACvC,4FAA4F;QAC5F,OAAO,EAAE,MAAM,SAAS,CAAC;KAC1B;IAUH;;;OAGG;IACH,KAAK,IAAI,IAAI;IAcb,6DAA6D;IAC7D,IAAI,IAAI,IAAI;IAOZ;;;OAGG;IACH,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAIpC;;;;OAIG;YACW,IAAI;CA6CnB"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export class HeartbeatScheduler {
|
|
2
|
+
client;
|
|
3
|
+
cfg;
|
|
4
|
+
timer = null;
|
|
5
|
+
status = "healthy";
|
|
6
|
+
onKillSwitch;
|
|
7
|
+
onRecover;
|
|
8
|
+
onPolicyChange;
|
|
9
|
+
onModeChange;
|
|
10
|
+
onTargetVersion;
|
|
11
|
+
getMode;
|
|
12
|
+
lastPolicyVersion = "";
|
|
13
|
+
lastMode = "";
|
|
14
|
+
lastTargetVersion = undefined;
|
|
15
|
+
/** Tracks whether the kill switch was active on the last heartbeat response. */
|
|
16
|
+
killSwitchActive = false;
|
|
17
|
+
/**
|
|
18
|
+
* @param client - Transport used to deliver heartbeats.
|
|
19
|
+
* @param cfg - Validated config. Provides identity, mode and interval.
|
|
20
|
+
* @param handlers - Callbacks fired when the backend signals a kill
|
|
21
|
+
* switch, a policy change, or a mode change.
|
|
22
|
+
*/
|
|
23
|
+
constructor(client, cfg, handlers) {
|
|
24
|
+
this.client = client;
|
|
25
|
+
this.cfg = cfg;
|
|
26
|
+
this.onKillSwitch = handlers.onKillSwitch;
|
|
27
|
+
this.onRecover = handlers.onRecover;
|
|
28
|
+
this.onPolicyChange = handlers.onPolicyChange;
|
|
29
|
+
this.onModeChange = handlers.onModeChange;
|
|
30
|
+
this.onTargetVersion = handlers.onTargetVersion;
|
|
31
|
+
this.getMode = handlers.getMode;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Send an immediate heartbeat and arm the periodic loop. Idempotent -
|
|
35
|
+
* calling `start` twice has no extra effect.
|
|
36
|
+
*/
|
|
37
|
+
start() {
|
|
38
|
+
if (this.timer)
|
|
39
|
+
return;
|
|
40
|
+
this.beat().catch(() => { });
|
|
41
|
+
this.timer = setInterval(() => {
|
|
42
|
+
this.beat().catch(() => { });
|
|
43
|
+
}, this.cfg.heartbeatIntervalMs);
|
|
44
|
+
if (this.timer.unref) {
|
|
45
|
+
this.timer.unref();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Cancel the periodic loop. Safe to call multiple times. */
|
|
49
|
+
stop() {
|
|
50
|
+
if (this.timer) {
|
|
51
|
+
clearInterval(this.timer);
|
|
52
|
+
this.timer = null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Override the self-reported status sent on the next beat. Used by the
|
|
57
|
+
* agent when subsystems degrade.
|
|
58
|
+
*/
|
|
59
|
+
setStatus(status) {
|
|
60
|
+
this.status = status;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Send one heartbeat, then react to the response:
|
|
64
|
+
* - `killSwitch` → stop the loop and fire {@link onKillSwitch}.
|
|
65
|
+
* - `policyVersion` change → cache it and fire {@link onPolicyChange}.
|
|
66
|
+
*/
|
|
67
|
+
async beat() {
|
|
68
|
+
const payload = {
|
|
69
|
+
projectId: this.cfg.projectId,
|
|
70
|
+
agentId: this.cfg.agentId,
|
|
71
|
+
agentVersion: this.cfg.agentVersion,
|
|
72
|
+
runtime: this.cfg.runtime,
|
|
73
|
+
framework: this.cfg.framework,
|
|
74
|
+
status: this.status,
|
|
75
|
+
mode: this.getMode(),
|
|
76
|
+
timestamp: new Date().toISOString(),
|
|
77
|
+
};
|
|
78
|
+
const res = await this.client.sendHeartbeat(payload);
|
|
79
|
+
if (!res)
|
|
80
|
+
return;
|
|
81
|
+
if (res.killSwitch && !this.killSwitchActive) {
|
|
82
|
+
this.killSwitchActive = true;
|
|
83
|
+
this.onKillSwitch();
|
|
84
|
+
}
|
|
85
|
+
else if (!res.killSwitch && this.killSwitchActive) {
|
|
86
|
+
this.killSwitchActive = false;
|
|
87
|
+
this.onRecover?.();
|
|
88
|
+
}
|
|
89
|
+
if (res.policyVersion && res.policyVersion !== this.lastPolicyVersion) {
|
|
90
|
+
this.lastPolicyVersion = res.policyVersion;
|
|
91
|
+
if (this.lastPolicyVersion) {
|
|
92
|
+
this.onPolicyChange(res.policyVersion);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (res.mode && res.mode !== this.lastMode) {
|
|
96
|
+
this.lastMode = res.mode;
|
|
97
|
+
this.onModeChange(res.mode);
|
|
98
|
+
}
|
|
99
|
+
if (this.onTargetVersion && res.targetVersion !== this.lastTargetVersion) {
|
|
100
|
+
this.lastTargetVersion = res.targetVersion ?? null;
|
|
101
|
+
this.onTargetVersion({
|
|
102
|
+
targetVersion: res.targetVersion ?? null,
|
|
103
|
+
upgradeAvailable: res.upgradeAvailable ?? false,
|
|
104
|
+
changelog: res.changelog ?? null,
|
|
105
|
+
impact: res.impact ?? null,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=heartbeat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heartbeat.js","sourceRoot":"","sources":["../../src/transport/heartbeat.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface TlsOptions {
|
|
2
|
+
/** PEM trust anchor (private CA) added to the default roots. */
|
|
3
|
+
caCert?: string;
|
|
4
|
+
/** Client certificate (PEM) presented for mutual TLS. */
|
|
5
|
+
clientCert?: string;
|
|
6
|
+
/** Client private key (PEM) for mutual TLS. */
|
|
7
|
+
clientKey?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Pinned server certificate SHA-256 fingerprints (hex, with or without
|
|
10
|
+
* colons, case-insensitive). When set, the server cert must match one.
|
|
11
|
+
*/
|
|
12
|
+
collectorFingerprints?: string[];
|
|
13
|
+
/** Reject self-signed/untrusted certs. Default true. */
|
|
14
|
+
rejectUnauthorized?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface SecureRequestInput {
|
|
17
|
+
method: "GET" | "POST";
|
|
18
|
+
url: string;
|
|
19
|
+
headers: Record<string, string>;
|
|
20
|
+
body?: string;
|
|
21
|
+
timeoutMs: number;
|
|
22
|
+
tls: TlsOptions;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Perform a pinned HTTPS (or plain HTTP for non-https URLs) JSON request.
|
|
26
|
+
* Resolves with the parsed JSON body, rejects on transport / non-2xx / pinning
|
|
27
|
+
* failures.
|
|
28
|
+
*/
|
|
29
|
+
export declare function secureRequest(input: SecureRequestInput): Promise<unknown>;
|
|
30
|
+
//# sourceMappingURL=secure-request.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secure-request.d.ts","sourceRoot":"","sources":["../../src/transport/secure-request.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,UAAU;IACzB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;IACjC,wDAAwD;IACxD,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAWD,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,UAAU,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAmEzE"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pinned HTTPS transport (the codeable slice of mTLS - Addendum E.4.2).
|
|
3
|
+
*
|
|
4
|
+
* Used in place of `fetch` when the agent is configured with {@link TlsOptions}.
|
|
5
|
+
* It provides two guarantees that global `fetch` cannot easily express:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Server certificate pinning**: the collector's certificate must match
|
|
8
|
+
* one of the pinned SHA-256 fingerprints. This defeats a rogue CA or a
|
|
9
|
+
* TLS-terminating proxy injected between the agent and the collector.
|
|
10
|
+
* 2. **Mutual TLS**: the agent presents a client certificate/key so the
|
|
11
|
+
* collector can authenticate the agent at the transport layer.
|
|
12
|
+
*
|
|
13
|
+
* A private CA can be trusted via `caCert`. Everything is fail-closed: a
|
|
14
|
+
* fingerprint mismatch or TLS error rejects the request.
|
|
15
|
+
*/
|
|
16
|
+
import https from "node:https";
|
|
17
|
+
import http from "node:http";
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
function normalizeFp(fp) {
|
|
20
|
+
return fp.replace(/:/g, "").toLowerCase();
|
|
21
|
+
}
|
|
22
|
+
function certFingerprint(cert) {
|
|
23
|
+
// cert.raw is the DER-encoded certificate.
|
|
24
|
+
return createHash("sha256").update(cert.raw).digest("hex");
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Perform a pinned HTTPS (or plain HTTP for non-https URLs) JSON request.
|
|
28
|
+
* Resolves with the parsed JSON body, rejects on transport / non-2xx / pinning
|
|
29
|
+
* failures.
|
|
30
|
+
*/
|
|
31
|
+
export function secureRequest(input) {
|
|
32
|
+
const { method, url, headers, body, timeoutMs, tls } = input;
|
|
33
|
+
const u = new URL(url);
|
|
34
|
+
const isHttps = u.protocol === "https:";
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const pinned = (tls.collectorFingerprints ?? []).map(normalizeFp);
|
|
37
|
+
const options = {
|
|
38
|
+
method,
|
|
39
|
+
hostname: u.hostname,
|
|
40
|
+
port: u.port || (isHttps ? 443 : 80),
|
|
41
|
+
path: u.pathname + u.search,
|
|
42
|
+
headers,
|
|
43
|
+
...(isHttps
|
|
44
|
+
? {
|
|
45
|
+
ca: tls.caCert,
|
|
46
|
+
cert: tls.clientCert,
|
|
47
|
+
key: tls.clientKey,
|
|
48
|
+
rejectUnauthorized: tls.rejectUnauthorized ?? true,
|
|
49
|
+
// Pin the server certificate fingerprint.
|
|
50
|
+
checkServerIdentity: (host, cert) => {
|
|
51
|
+
if (pinned.length === 0)
|
|
52
|
+
return undefined;
|
|
53
|
+
const fp = certFingerprint(cert);
|
|
54
|
+
if (!pinned.includes(fp)) {
|
|
55
|
+
return new Error(`[rasp-transport] collector certificate fingerprint not pinned (host ${host})`);
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
: {}),
|
|
61
|
+
};
|
|
62
|
+
const transport = isHttps ? https : http;
|
|
63
|
+
const req = transport.request(options, (res) => {
|
|
64
|
+
const chunks = [];
|
|
65
|
+
res.on("data", (c) => chunks.push(c));
|
|
66
|
+
res.on("end", () => {
|
|
67
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
68
|
+
const status = res.statusCode ?? 0;
|
|
69
|
+
if (status < 200 || status >= 300) {
|
|
70
|
+
reject(new Error(`[rasp-transport] ${u.pathname} → HTTP ${status}: ${text}`));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
resolve(text ? JSON.parse(text) : {});
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
reject(err);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
req.setTimeout(timeoutMs, () => req.destroy(new Error("[rasp-transport] request timeout")));
|
|
82
|
+
req.on("error", reject);
|
|
83
|
+
// For mutual TLS, surface a clear error if the handshake is rejected.
|
|
84
|
+
req.on("secureConnect", () => {
|
|
85
|
+
const socket = req.socket;
|
|
86
|
+
if (socket && isHttps && (tls.rejectUnauthorized ?? true) && !socket.authorized) {
|
|
87
|
+
req.destroy(new Error(`[rasp-transport] TLS not authorized: ${socket.authorizationError}`));
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
if (body)
|
|
91
|
+
req.write(body);
|
|
92
|
+
req.end();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=secure-request.js.map
|