securenow 8.24.0 → 8.25.0
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/NPM_README.md +693 -305
- package/README.md +76 -28
- package/SKILL-API.md +648 -525
- package/SKILL-CLI.md +293 -663
- package/app-config.js +19 -1
- package/cidr.js +139 -20
- package/cli/init.js +77 -15
- package/cli/utils.js +26 -14
- package/cli.js +7 -5
- package/console-instrumentation.js +6 -0
- package/firewall-cloud.js +81 -32
- package/firewall-iptables.js +123 -62
- package/firewall-only.js +2 -0
- package/firewall-tcp.js +19 -5
- package/firewall.js +21 -1
- package/framework-routes.js +222 -0
- package/log-format.js +2 -0
- package/log-redaction.js +135 -0
- package/mcp/catalog.js +14 -9
- package/next-route.js +61 -0
- package/nextjs-middleware.js +8 -4
- package/nextjs-wrapper.js +8 -4
- package/nextjs.d.ts +35 -67
- package/nextjs.js +29 -6
- package/package.json +5 -2
- package/register.d.ts +27 -34
- package/request-capture.js +11 -6
- package/resolve-ip.js +23 -5
- package/sessions.d.ts +27 -0
- package/sessions.js +71 -10
- package/tracing.d.ts +40 -121
- package/tracing.js +12 -4
package/register.d.ts
CHANGED
|
@@ -1,61 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SecureNow Register Module TypeScript Declarations
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Preload for Node.js servers (Express, Fastify, NestJS, Koa, Hapi, Hono, Polka, Feathers, h3,
|
|
5
|
+
* raw node:http): `node -r securenow/register <entry>`. It starts tracing, logs, body capture,
|
|
6
|
+
* the firewall and (when configured) session revocation before the app loads.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
* Configuration:
|
|
10
|
-
*
|
|
11
|
-
* Local development reads .securenow/credentials.json. Run
|
|
12
|
-
* `npx securenow login` to choose/create an app, then `npx securenow init`
|
|
13
|
-
* to ensure secure defaults and explanations are present.
|
|
14
|
-
*
|
|
15
|
-
* Production uses the same file shape. Run
|
|
16
|
-
* `npx securenow credentials runtime --env production`, then mount/copy the
|
|
17
|
-
* generated JSON to .securenow/credentials.json in the running app.
|
|
10
|
+
* Configuration: .securenow/runtime.json, written by `npx securenow login`; `npx securenow init`
|
|
11
|
+
* adds the secure defaults with explanations.
|
|
18
12
|
*
|
|
19
|
-
*
|
|
13
|
+
* Production: `npx securenow credentials runtime --env production`, then mount/copy the file to
|
|
14
|
+
* .securenow/credentials.json in the running app, or store the `--format env` output
|
|
15
|
+
* (SECURENOW_CREDENTIALS) as a secret environment variable on hosts without file mounts.
|
|
16
|
+
* SECURENOW_DISABLED=1 turns the SDK off (tests).
|
|
20
17
|
*/
|
|
21
18
|
|
|
22
19
|
// This module has side effects (initializes OpenTelemetry)
|
|
23
|
-
// It's typically
|
|
20
|
+
// It's typically preloaded with -r, not imported
|
|
24
21
|
export {};
|
|
25
22
|
|
|
26
23
|
/**
|
|
27
24
|
* @example
|
|
28
25
|
* ```bash
|
|
29
|
-
* #
|
|
30
|
-
*
|
|
26
|
+
* # Preload (CommonJS, ESM and TypeScript alike)
|
|
27
|
+
* node -r securenow/register app.js
|
|
28
|
+
* node -r securenow/register --import tsx src/index.ts
|
|
29
|
+
* NODE_OPTIONS="-r securenow/register" npm start
|
|
31
30
|
* ```
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* ```bash
|
|
35
|
-
* # Using with PM2
|
|
36
|
-
* pm2 start app.js --node-args="-r securenow/register"
|
|
37
|
-
* ```
|
|
38
|
-
*
|
|
31
|
+
*
|
|
39
32
|
* @example
|
|
40
33
|
* ```javascript
|
|
41
|
-
* // ecosystem.config.
|
|
34
|
+
* // ecosystem.config.cjs (PM2, fork or cluster mode)
|
|
42
35
|
* module.exports = {
|
|
43
36
|
* apps: [{
|
|
44
37
|
* name: 'my-app',
|
|
45
38
|
* script: './app.js',
|
|
46
|
-
*
|
|
47
|
-
* //
|
|
39
|
+
* // An absolute path: a bare '-r securenow/register' is resolved from the PM2 daemon's
|
|
40
|
+
* // folder in cluster mode, and the workers crash-loop with "Cannot find module".
|
|
41
|
+
* node_args: ['-r', require.resolve('securenow/register')],
|
|
42
|
+
* instances: 'max',
|
|
43
|
+
* exec_mode: 'cluster',
|
|
48
44
|
* }]
|
|
49
45
|
* };
|
|
50
46
|
* ```
|
|
51
|
-
*
|
|
47
|
+
*
|
|
52
48
|
* @example
|
|
53
|
-
* ```
|
|
54
|
-
* //
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* import express from 'express';
|
|
58
|
-
* const app = express();
|
|
59
|
-
* // Your app code...
|
|
49
|
+
* ```javascript
|
|
50
|
+
* // First line of a CommonJS entry (alternative to -r)
|
|
51
|
+
* require('securenow/register');
|
|
52
|
+
* const express = require('express');
|
|
60
53
|
* ```
|
|
61
54
|
*/
|
package/request-capture.js
CHANGED
|
@@ -44,18 +44,21 @@ const DEFAULT_SENSITIVE_FIELDS = [
|
|
|
44
44
|
// and they only ever transform captured telemetry strings (read-only) — never
|
|
45
45
|
// the actual request/response stream. Compiled once at module load.
|
|
46
46
|
const VALUE_REDACTORS = [
|
|
47
|
+
// The JWT and email patterns only start where a run of their characters starts (the
|
|
48
|
+
// lookbehind), so a long run that holds no match is scanned once, not once per position:
|
|
49
|
+
// a crafted 10KB value costs microseconds, not 50ms+ of event loop.
|
|
47
50
|
// JWT: three base64url segments. Anchored to the eyJ header so it won't
|
|
48
51
|
// match arbitrary dotted tokens.
|
|
49
|
-
{ name: 'jwt', re: /eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}/g },
|
|
52
|
+
{ name: 'jwt', hint: 'eyJ', re: /(?<![A-Za-z0-9_-])eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}/g },
|
|
50
53
|
// Bearer/Basic auth header value embedded in a body string.
|
|
51
54
|
{ name: 'bearer', re: /\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi },
|
|
52
55
|
// Stripe-style / SecureNow live+test API keys.
|
|
53
56
|
{ name: 'apikey', re: /\b(?:sk|pk|rk|snk)_(?:live|test)_[A-Za-z0-9]{8,}/g },
|
|
54
|
-
// Email addresses (bounded local/domain parts).
|
|
55
|
-
{ name: 'email', re:
|
|
57
|
+
// Email addresses (bounded local/domain parts: 64 and 253 characters at most).
|
|
58
|
+
{ name: 'email', hint: '@', re: /(?<![\w.%+-])[\w.%+-]{1,64}@[A-Za-z0-9.-]{1,253}\.[A-Za-z]{2,24}\b/g },
|
|
56
59
|
// Credit-card-like: 13–19 digit runs that pass a Luhn check, allowing
|
|
57
60
|
// space/dash grouping. Luhn-gated to avoid clobbering ordinary long numbers.
|
|
58
|
-
{ name: 'card', re: /\b(?:\d[ -]?){
|
|
61
|
+
{ name: 'card', re: /\b(?:\d[ -]?){12,18}\d\b/g, luhn: true },
|
|
59
62
|
];
|
|
60
63
|
|
|
61
64
|
// Skip the value-shape scan on very large strings to bound per-body cost.
|
|
@@ -77,15 +80,17 @@ function luhnValid(digits) {
|
|
|
77
80
|
/**
|
|
78
81
|
* Redact obvious secret/PII shapes inside a captured string VALUE.
|
|
79
82
|
* Returns the input unchanged when nothing matches (cheap common case).
|
|
83
|
+
* maxScanLength: longer values skip the shape scan (log lines pass a larger bound).
|
|
80
84
|
*/
|
|
81
|
-
function redactSensitiveValue(value) {
|
|
85
|
+
function redactSensitiveValue(value, maxScanLength = MAX_VALUE_SCAN_LENGTH) {
|
|
82
86
|
if (typeof value !== 'string' || value.length === 0) return value;
|
|
83
87
|
// Credential assignments in GraphQL/query strings need field-aware
|
|
84
88
|
// redaction even though a short numeric OTP has no unique value shape.
|
|
85
89
|
value = redactOtpQuery(redactOtpGraphQL(value));
|
|
86
|
-
if (value.length >
|
|
90
|
+
if (value.length > maxScanLength) return value; // bound shape scans
|
|
87
91
|
let out = value;
|
|
88
92
|
for (const r of VALUE_REDACTORS) {
|
|
93
|
+
if (r.hint && !out.includes(r.hint)) continue;
|
|
89
94
|
r.re.lastIndex = 0;
|
|
90
95
|
if (r.luhn) {
|
|
91
96
|
out = out.replace(r.re, (m) => {
|
package/resolve-ip.js
CHANGED
|
@@ -7,17 +7,19 @@ const { createMatcher } = require('./cidr');
|
|
|
7
7
|
const LOOPBACK_RE = /^(127\.|::1$|::ffff:127\.)/;
|
|
8
8
|
const PRIVATE_IP_RE = /^(127\.|::1$|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.|f[cd][0-9a-f]{2}:)/i;
|
|
9
9
|
|
|
10
|
-
// Named proxy presets for config.networking.trustedProxies. Cloudflare's published
|
|
11
|
-
// (https://www.cloudflare.com/ips-v4): an app behind Cloudflare trusts them with "cloudflare".
|
|
10
|
+
// Named proxy presets for config.networking.trustedProxies. Cloudflare's published edge ranges
|
|
11
|
+
// (https://www.cloudflare.com/ips-v4 and /ips-v6): an app behind Cloudflare trusts them with "cloudflare".
|
|
12
12
|
const PROXY_PRESETS = {
|
|
13
13
|
cloudflare: [
|
|
14
14
|
'173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22', '141.101.64.0/18',
|
|
15
15
|
'108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20', '197.234.240.0/22', '198.41.128.0/17',
|
|
16
16
|
'162.158.0.0/15', '104.16.0.0/13', '104.24.0.0/14', '172.64.0.0/13', '131.0.72.0/22',
|
|
17
|
+
'2400:cb00::/32', '2606:4700::/32', '2803:f800::/32', '2405:b500::/32', '2405:8100::/32',
|
|
18
|
+
'2a06:98c0::/29', '2c0f:f248::/32',
|
|
17
19
|
],
|
|
18
20
|
};
|
|
19
21
|
|
|
20
|
-
// Exact addresses (IPv4 or IPv6),
|
|
22
|
+
// Exact addresses and CIDR ranges (IPv4 or IPv6), and presets.
|
|
21
23
|
function trustedProxyMatcher(entries) {
|
|
22
24
|
const expanded = [];
|
|
23
25
|
for (const entry of entries) {
|
|
@@ -93,14 +95,30 @@ function getHeader(headers, name) {
|
|
|
93
95
|
return firstHeaderValue(headers[name] || headers[name.toLowerCase()] || headers[name.toUpperCase()]);
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
// A request that came over a Unix domain socket or a Windows named pipe has no remote address, and only a
|
|
99
|
+
// process on this machine can connect to one: nginx `proxy_pass http://unix:/run/app.sock`, Nuxt's dev
|
|
100
|
+
// proxy, a local sidecar. Such a connection is a trusted local proxy, so its forwarding headers count
|
|
101
|
+
// (before this, requests over a socket had no client IP at all and IP blocking never applied to them).
|
|
102
|
+
function arrivedOverLocalSocket(request) {
|
|
103
|
+
const socket = request && request.socket;
|
|
104
|
+
if (!socket || socket.remoteAddress) return false;
|
|
105
|
+
try {
|
|
106
|
+
const server = socket.server || socket._server;
|
|
107
|
+
return !!server && typeof server.address === 'function' && typeof server.address() === 'string';
|
|
108
|
+
} catch (_) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
96
113
|
function resolveClientIpWithDetails(request) {
|
|
97
114
|
const socketIp = normalizeIp(request.socket?.remoteAddress || '');
|
|
98
115
|
const headers = request.headers || {};
|
|
99
|
-
const
|
|
116
|
+
const viaLocalSocket = !socketIp && arrivedOverLocalSocket(request);
|
|
117
|
+
const trustedProxy = viaLocalSocket || isFromTrustedProxy(socketIp);
|
|
100
118
|
|
|
101
119
|
const details = {
|
|
102
120
|
ip: socketIp,
|
|
103
|
-
source: socketIp ? 'socket' : 'unknown',
|
|
121
|
+
source: socketIp ? 'socket' : (viaLocalSocket ? 'unix-socket' : 'unknown'),
|
|
104
122
|
socketIp,
|
|
105
123
|
trustedProxy,
|
|
106
124
|
forwardedFor: String(getHeader(headers, 'x-forwarded-for') || ''),
|
package/sessions.d.ts
CHANGED
|
@@ -30,6 +30,33 @@ export function guard(options?: GuardOptions): (req: any, res: any, next: any) =
|
|
|
30
30
|
/** Express-style middleware that ONLY emits session.seen (detection feed, no enforcement). */
|
|
31
31
|
export function capture(options?: GuardOptions): (req: any, res: any, next: any) => void;
|
|
32
32
|
|
|
33
|
+
export interface CheckOptions {
|
|
34
|
+
/** Extract the session id. Defaults to the hashed Bearer token or session cookie. */
|
|
35
|
+
getSessionId?: (req: any) => string | null | undefined;
|
|
36
|
+
/** Extract the user id. Defaults to the JWT `sub` claim. */
|
|
37
|
+
getUserId?: (req: any) => string | null | undefined;
|
|
38
|
+
/** Client IP for session.seen when `req` has no socket (a Web Request). */
|
|
39
|
+
ip?: string;
|
|
40
|
+
/** Emit session.seen for detection (default true). */
|
|
41
|
+
capture?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* For code that cannot use guard() (Fastify hooks, Koa/Hono middleware, Next.js route handlers, h3):
|
|
46
|
+
* is this request's session or user revoked? `req` is a Node request or a Web `Request`. Fail-open. (8.25.0+)
|
|
47
|
+
*/
|
|
48
|
+
export function check(req: IncomingMessage | Request | { headers: any }, options?: CheckOptions): {
|
|
49
|
+
revoked: boolean;
|
|
50
|
+
sessionId: string | null;
|
|
51
|
+
userId: string | null;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Zero-code guard/capture for every framework, installed by the SDK from config.sessions.guard /
|
|
56
|
+
* config.sessions.capture in .securenow/runtime.json. Returns true when it installed the hook. (8.25.0+)
|
|
57
|
+
*/
|
|
58
|
+
export function installHttpGuard(options?: { guard?: boolean; capture?: boolean }): boolean;
|
|
59
|
+
|
|
33
60
|
/** Resolve the client IP from a request (X-Forwarded-For / CF / socket). */
|
|
34
61
|
export function resolveClientIp(req: any): string | null;
|
|
35
62
|
|
package/sessions.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
// half of account-takeover response).
|
|
5
5
|
//
|
|
6
6
|
// const securenow = require('securenow/sessions');
|
|
7
|
-
// app.use(securenow.guard()); // auto-detects NextAuth/connect.sid/JWT sessions
|
|
8
|
-
// // ...
|
|
9
|
-
// if (securenow.
|
|
7
|
+
// app.use(securenow.guard()); // Express-style; auto-detects NextAuth/connect.sid/JWT sessions
|
|
8
|
+
// // ...any other framework (Fastify, Koa, Hono, Next.js route handlers, h3):
|
|
9
|
+
// if (securenow.check(req).revoked) return /* a 401 */;
|
|
10
|
+
// // ...or no code at all: "sessions": { "guard": true, "capture": true } in .securenow/runtime.json
|
|
10
11
|
//
|
|
11
12
|
// The SDK pulls active revocations from SecureNow (outbound, ETag-cached) and
|
|
12
13
|
// checks each request locally — no inbound webhook, works behind NAT, portable
|
|
@@ -150,6 +151,21 @@ const SESSION_COOKIES = [
|
|
|
150
151
|
'sid',
|
|
151
152
|
];
|
|
152
153
|
|
|
154
|
+
// Session cookies: config.sessions.cookieNames first (the app's own), then the defaults.
|
|
155
|
+
function sessionCookieNames() {
|
|
156
|
+
let extra = [];
|
|
157
|
+
try { extra = appConfig.listConfig('sessions.cookieNames'); } catch {}
|
|
158
|
+
return extra.length ? [...extra, ...SESSION_COOKIES] : SESSION_COOKIES;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// A header from a Node request (plain object) or a Web Request (Headers).
|
|
162
|
+
function headerOf(req, name) {
|
|
163
|
+
const headers = req && req.headers;
|
|
164
|
+
if (!headers) return '';
|
|
165
|
+
if (typeof headers.get === 'function') return headers.get(name) || '';
|
|
166
|
+
return headers[name] || '';
|
|
167
|
+
}
|
|
168
|
+
|
|
153
169
|
function parseCookies(header) {
|
|
154
170
|
const out = {};
|
|
155
171
|
if (!header) return out;
|
|
@@ -165,8 +181,7 @@ function parseCookies(header) {
|
|
|
165
181
|
function defaultExtract(req) {
|
|
166
182
|
let sessionId = null;
|
|
167
183
|
let userId = null;
|
|
168
|
-
const
|
|
169
|
-
const auth = headers.authorization || headers.Authorization;
|
|
184
|
+
const auth = headerOf(req, 'authorization') || headerOf(req, 'Authorization');
|
|
170
185
|
if (auth && /^Bearer\s+/i.test(auth)) {
|
|
171
186
|
const token = auth.replace(/^Bearer\s+/i, '').trim();
|
|
172
187
|
if (token) {
|
|
@@ -178,9 +193,10 @@ function defaultExtract(req) {
|
|
|
178
193
|
} catch {}
|
|
179
194
|
}
|
|
180
195
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
196
|
+
const cookieHeader = headerOf(req, 'cookie');
|
|
197
|
+
if (!sessionId && cookieHeader) {
|
|
198
|
+
const cookies = parseCookies(cookieHeader);
|
|
199
|
+
for (const name of sessionCookieNames()) {
|
|
184
200
|
if (cookies[name]) {
|
|
185
201
|
sessionId = sha256(cookies[name]);
|
|
186
202
|
break;
|
|
@@ -255,7 +271,7 @@ function capture(options = {}) {
|
|
|
255
271
|
|
|
256
272
|
function defaultOnRevoked(req, res) {
|
|
257
273
|
try {
|
|
258
|
-
const clears =
|
|
274
|
+
const clears = sessionCookieNames().map((n) => `${n}=; Path=/; Max-Age=0; HttpOnly`);
|
|
259
275
|
if (typeof res.setHeader === 'function') res.setHeader('Set-Cookie', clears);
|
|
260
276
|
} catch {}
|
|
261
277
|
if (typeof res.status === 'function' && typeof res.json === 'function') {
|
|
@@ -298,4 +314,49 @@ function guard(options = {}) {
|
|
|
298
314
|
};
|
|
299
315
|
}
|
|
300
316
|
|
|
301
|
-
|
|
317
|
+
/**
|
|
318
|
+
* For code that cannot use guard() (Fastify hooks, Koa or Hono middleware, Next.js route handlers, h3):
|
|
319
|
+
* is this request's session or user revoked? `req` is a Node request or a Web Request. Feeds
|
|
320
|
+
* session-theft detection (session.seen) unless options.capture is false. Fail-open.
|
|
321
|
+
*
|
|
322
|
+
* @param {object} req
|
|
323
|
+
* @param {{ getSessionId?: (req)=>string, getUserId?: (req)=>string, ip?: string, capture?: boolean }} [options]
|
|
324
|
+
* @returns {{ revoked: boolean, sessionId: string|null, userId: string|null }}
|
|
325
|
+
*/
|
|
326
|
+
function check(req, options = {}) {
|
|
327
|
+
try {
|
|
328
|
+
if (require('./sdk-disabled').sdkDisabled('sessions check')) return { revoked: false, sessionId: null, userId: null };
|
|
329
|
+
start(options);
|
|
330
|
+
const { sessionId, userId } = extractIdentity(req, options.getSessionId, options.getUserId);
|
|
331
|
+
if (options.capture !== false) emitSessionSeen(sessionId, userId, options.ip || resolveClientIp(req));
|
|
332
|
+
return { revoked: isRevoked({ sessionId, userId }), sessionId: sessionId || null, userId: userId || null };
|
|
333
|
+
} catch {
|
|
334
|
+
return { revoked: false, sessionId: null, userId: null };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Zero code, every framework (config.sessions.guard / config.sessions.capture): the firewall's
|
|
339
|
+
// request hook runs this on each incoming request before the app sees it. guard answers a revoked
|
|
340
|
+
// session or user with a 401 and clears the session cookies; capture feeds session-theft detection.
|
|
341
|
+
let _httpGuardInstalled = false;
|
|
342
|
+
function installHttpGuard(options = {}) {
|
|
343
|
+
const enforce = !!options.guard;
|
|
344
|
+
const capture = !!options.capture;
|
|
345
|
+
if ((!enforce && !capture) || _httpGuardInstalled) return false;
|
|
346
|
+
if (require('./sdk-disabled').sdkDisabled('sessions')) return false;
|
|
347
|
+
_httpGuardInstalled = true;
|
|
348
|
+
start(options);
|
|
349
|
+
require('./firewall').addRequestHook(function securenowSessionHook(req, res) {
|
|
350
|
+
const { sessionId, userId } = defaultExtract(req);
|
|
351
|
+
if (!sessionId && !userId) return false;
|
|
352
|
+
if (capture) emitSessionSeen(sessionId, userId, resolveClientIp(req));
|
|
353
|
+
if (enforce && isRevoked({ sessionId, userId })) {
|
|
354
|
+
defaultOnRevoked(req, res);
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
return false;
|
|
358
|
+
});
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
module.exports = { start, syncOnce, isRevoked, guard, capture, check, installHttpGuard, resolveClientIp, sha256 };
|
package/tracing.d.ts
CHANGED
|
@@ -1,74 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SecureNow Tracing Module TypeScript Declarations
|
|
3
|
-
*
|
|
4
|
-
* Core tracing
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Default sensitive fields that are automatically redacted from request bodies
|
|
10
|
-
*/
|
|
11
|
-
export const DEFAULT_SENSITIVE_FIELDS: readonly string[];
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Redact sensitive fields from an object (recursively)
|
|
15
|
-
*
|
|
16
|
-
* @param obj - Object to redact (can be nested)
|
|
17
|
-
* @param sensitiveFields - Array of field names to redact (case-insensitive substring match)
|
|
18
|
-
* @returns Redacted copy of the object
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* ```typescript
|
|
22
|
-
* import { redactSensitiveData } from 'securenow/tracing';
|
|
23
|
-
*
|
|
24
|
-
* const data = {
|
|
25
|
-
* email: 'user@example.com',
|
|
26
|
-
* password: 'secret123',
|
|
27
|
-
* nested: {
|
|
28
|
-
* api_key: 'sk_live_abc123'
|
|
29
|
-
* }
|
|
30
|
-
* };
|
|
31
|
-
*
|
|
32
|
-
* const redacted = redactSensitiveData(data);
|
|
33
|
-
* // Result: {
|
|
34
|
-
* // email: 'user@example.com',
|
|
35
|
-
* // password: '[REDACTED]',
|
|
36
|
-
* // nested: { api_key: '[REDACTED]' }
|
|
37
|
-
* // }
|
|
38
|
-
* ```
|
|
39
|
-
*/
|
|
40
|
-
export function redactSensitiveData<T = any>(
|
|
41
|
-
obj: T,
|
|
42
|
-
sensitiveFields?: string[]
|
|
43
|
-
): T;
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Redact sensitive data from GraphQL query strings
|
|
47
|
-
*
|
|
48
|
-
* @param query - GraphQL query string
|
|
49
|
-
* @param sensitiveFields - Array of field names to redact
|
|
50
|
-
* @returns Redacted query string
|
|
51
|
-
*
|
|
52
|
-
* @example
|
|
53
|
-
* ```typescript
|
|
54
|
-
* import { redactGraphQLQuery } from 'securenow/tracing';
|
|
55
|
-
*
|
|
56
|
-
* const query = `
|
|
57
|
-
* mutation {
|
|
58
|
-
* login(email: "user@example.com", password: "secret123") {
|
|
59
|
-
* token
|
|
60
|
-
* }
|
|
61
|
-
* }
|
|
62
|
-
* `;
|
|
63
|
-
*
|
|
64
|
-
* const redacted = redactGraphQLQuery(query);
|
|
65
|
-
* // Result: mutation { login(email: "user@example.com", password: "[REDACTED]") { token } }
|
|
66
|
-
* ```
|
|
3
|
+
*
|
|
4
|
+
* Core tracing for Node.js applications. It starts when loaded, usually through
|
|
5
|
+
* `node -r securenow/register`; import it directly only for the logger API below.
|
|
6
|
+
* Redaction helpers live in `securenow/nextjs-middleware` and the `securenow redact` CLI.
|
|
67
7
|
*/
|
|
68
|
-
export function redactGraphQLQuery(
|
|
69
|
-
query: string,
|
|
70
|
-
sensitiveFields?: string[]
|
|
71
|
-
): string;
|
|
72
8
|
|
|
73
9
|
/**
|
|
74
10
|
* OpenTelemetry Logger interface
|
|
@@ -86,19 +22,20 @@ export interface LogRecord {
|
|
|
86
22
|
* 5 = DEBUG, 9 = INFO, 13 = WARN, 17 = ERROR
|
|
87
23
|
*/
|
|
88
24
|
severityNumber: number;
|
|
89
|
-
|
|
25
|
+
|
|
90
26
|
/**
|
|
91
27
|
* Human-readable severity text
|
|
92
28
|
*/
|
|
93
29
|
severityText: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | string;
|
|
94
|
-
|
|
30
|
+
|
|
95
31
|
/**
|
|
96
|
-
* Log message body
|
|
32
|
+
* Log message body. Secret shapes (JWTs, Bearer credentials, API keys, card numbers, emails,
|
|
33
|
+
* OTP assignments) are redacted before export unless config.logging.redact is false.
|
|
97
34
|
*/
|
|
98
35
|
body: string;
|
|
99
|
-
|
|
36
|
+
|
|
100
37
|
/**
|
|
101
|
-
* Structured attributes for filtering/searching
|
|
38
|
+
* Structured attributes for filtering/searching (exported as set)
|
|
102
39
|
*/
|
|
103
40
|
attributes?: Record<string, any>;
|
|
104
41
|
}
|
|
@@ -112,75 +49,57 @@ export interface LoggerProvider {
|
|
|
112
49
|
}
|
|
113
50
|
|
|
114
51
|
/**
|
|
115
|
-
* Get a logger
|
|
116
|
-
*
|
|
117
|
-
* @param name - Logger name (e.g., 'my-service', 'auth-module')
|
|
52
|
+
* Get a logger that sends structured logs to SecureNow, correlated with the active trace.
|
|
53
|
+
*
|
|
54
|
+
* @param name - Logger name (e.g., 'my-service', 'auth-module'); defaults to 'default'
|
|
118
55
|
* @param version - Logger version (optional, defaults to '1.0.0')
|
|
119
|
-
* @returns Logger instance or null
|
|
120
|
-
*
|
|
56
|
+
* @returns Logger instance, or null when logging is off (config.logging.enabled=false) or the SDK
|
|
57
|
+
* did not start (SECURENOW_DISABLED=1)
|
|
58
|
+
*
|
|
121
59
|
* @example
|
|
122
60
|
* ```typescript
|
|
123
61
|
* import { getLogger } from 'securenow/tracing';
|
|
124
|
-
*
|
|
62
|
+
*
|
|
125
63
|
* const logger = getLogger('my-service', '1.0.0');
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* userId: 123,
|
|
134
|
-
* username: 'john',
|
|
135
|
-
* },
|
|
136
|
-
* });
|
|
137
|
-
* }
|
|
64
|
+
*
|
|
65
|
+
* logger?.emit({
|
|
66
|
+
* severityNumber: 9,
|
|
67
|
+
* severityText: 'INFO',
|
|
68
|
+
* body: 'User logged in',
|
|
69
|
+
* attributes: { userId: 123 },
|
|
70
|
+
* });
|
|
138
71
|
* ```
|
|
139
72
|
*/
|
|
140
73
|
export function getLogger(name?: string, version?: string): Logger | null;
|
|
141
74
|
|
|
142
75
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* @returns true when logging resolves enabled from .securenow/credentials.json defaults
|
|
146
|
-
*
|
|
147
|
-
* @example
|
|
148
|
-
* ```typescript
|
|
149
|
-
* import { isLoggingEnabled } from 'securenow/tracing';
|
|
150
|
-
*
|
|
151
|
-
* if (isLoggingEnabled()) {
|
|
152
|
-
* console.log('Logging is enabled');
|
|
153
|
-
* }
|
|
154
|
-
* ```
|
|
76
|
+
* Whether logs are exported (config.logging.enabled, default true; false when the SDK did not start).
|
|
155
77
|
*/
|
|
156
78
|
export function isLoggingEnabled(): boolean;
|
|
157
79
|
|
|
158
80
|
/**
|
|
159
|
-
* The OpenTelemetry LoggerProvider instance (
|
|
160
|
-
* Use getLogger() instead of accessing this directly
|
|
81
|
+
* The OpenTelemetry LoggerProvider instance (null when logging is off).
|
|
82
|
+
* Use getLogger() instead of accessing this directly.
|
|
161
83
|
*/
|
|
162
84
|
export const loggerProvider: LoggerProvider | null;
|
|
163
85
|
|
|
164
86
|
/**
|
|
165
|
-
* Configuration:
|
|
166
|
-
*
|
|
167
|
-
* Local development reads .securenow/credentials.json first. Run
|
|
168
|
-
* `npx securenow login` to write app identity/runtime API key and
|
|
169
|
-
* `npx securenow init` to ensure secure defaults and explanations.
|
|
87
|
+
* Configuration: .securenow/runtime.json, written by `npx securenow login`; `npx securenow init`
|
|
88
|
+
* adds the secure defaults with explanations.
|
|
170
89
|
*
|
|
171
|
-
* Production
|
|
172
|
-
*
|
|
173
|
-
*
|
|
90
|
+
* Production: `npx securenow credentials runtime --env production`, then mount/copy the file to
|
|
91
|
+
* .securenow/credentials.json in the running app. Hosts without file mounts store
|
|
92
|
+
* `npx securenow credentials runtime --env production --format env` (SECURENOW_CREDENTIALS) as a
|
|
93
|
+
* secret environment variable. No other SECURENOW_* variable is read; SECURENOW_DISABLED=1
|
|
94
|
+
* turns the SDK off (tests).
|
|
174
95
|
*
|
|
175
96
|
* Main config fields:
|
|
176
|
-
* - app.key / app.name
|
|
177
|
-
* - config.otel.endpoint only for advanced/self-hosted collector overrides
|
|
178
|
-
* - apiKey
|
|
97
|
+
* - app.key / app.name, apiKey (runtime API key)
|
|
179
98
|
* - config.runtime.deploymentEnvironment
|
|
180
|
-
* - config.logging.enabled
|
|
99
|
+
* - config.logging.enabled / redact
|
|
181
100
|
* - config.capture.body / multipart / maxBodySize / sensitiveFields
|
|
182
|
-
* - config.otel.endpoint / tracesEndpoint / logsEndpoint / headers
|
|
183
|
-
* - config.
|
|
184
|
-
*
|
|
185
|
-
*
|
|
101
|
+
* - config.otel.endpoint / tracesEndpoint / logsEndpoint / headers (self-hosted collectors only)
|
|
102
|
+
* - config.networking.trustedProxies
|
|
103
|
+
* - config.firewall.* (failMode, statusCode, tcp, iptables, cloud, rateLimitStore, ...)
|
|
104
|
+
* - config.sessions.guard / capture / cookieNames
|
|
186
105
|
*/
|
package/tracing.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// When the SDK does not start, the module still exports its API: getLogger() returns null, as it
|
|
4
|
+
// does with logging disabled, so app code that calls it keeps working under SECURENOW_DISABLED=1.
|
|
5
|
+
const NOT_STARTED = { loggerProvider: null, getLogger: () => null, isLoggingEnabled: () => false };
|
|
3
6
|
// Do nothing inside Node's internal ESM-hooks thread (see internal-thread.js).
|
|
4
|
-
if (require('./internal-thread').inInternalThread()) return;
|
|
7
|
+
if (require('./internal-thread').inInternalThread()) { module.exports = NOT_STARTED; return; }
|
|
5
8
|
// SECURENOW_DISABLED=1 turns the SDK off for this process (test runs).
|
|
6
|
-
if (require('./sdk-disabled').sdkDisabled('tracing')) return;
|
|
9
|
+
if (require('./sdk-disabled').sdkDisabled('tracing')) { module.exports = NOT_STARTED; return; }
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* Preload with: node --require securenow/register app.js
|
|
@@ -127,7 +130,7 @@ console.log('[securenow] pid=%d appId=%s instance=%s apiKey=%s credentials=%s
|
|
|
127
130
|
process.pid,
|
|
128
131
|
JSON.stringify(baseName),
|
|
129
132
|
JSON.stringify(endpointBase),
|
|
130
|
-
|
|
133
|
+
appConfig.resolveApiKey() ? 'set' : 'missing',
|
|
131
134
|
appConfig.describeCredentialsSource(),
|
|
132
135
|
serviceName,
|
|
133
136
|
serviceInstanceId
|
|
@@ -239,7 +242,8 @@ if (loggingEnabled) {
|
|
|
239
242
|
const batchLogProcessor = new BatchLogRecordProcessor({ exporter: logExporter });
|
|
240
243
|
loggerProvider = new LoggerProvider({
|
|
241
244
|
resource: sharedResource,
|
|
242
|
-
|
|
245
|
+
// Secret shapes in log lines are redacted before export (log-redaction.js).
|
|
246
|
+
processors: require('./log-redaction').logProcessors(appConfig, batchLogProcessor),
|
|
243
247
|
});
|
|
244
248
|
|
|
245
249
|
// Auto-patch console.* so every log/warn/error becomes an OTel log record
|
|
@@ -348,6 +352,8 @@ const sdk = new NodeSDK({
|
|
|
348
352
|
instrumentations: [
|
|
349
353
|
httpInstrumentation,
|
|
350
354
|
...(disabledMap['@opentelemetry/instrumentation-mongodb'] ? [] : [new MongoDBInstrumentation()]),
|
|
355
|
+
// http.route for Hono, h3, Feathers and Polka (no instrumentation in the OpenTelemetry bundle).
|
|
356
|
+
...(disabledMap['securenow-framework-routes'] ? [] : [new (require('./framework-routes').SecureNowRouteInstrumentation)()]),
|
|
351
357
|
...getNodeAutoInstrumentations({
|
|
352
358
|
...frameworkInstrumentationConfig,
|
|
353
359
|
...disabledMap,
|
|
@@ -404,6 +410,8 @@ const sdk = new NodeSDK({
|
|
|
404
410
|
aws: firewallOptions.aws,
|
|
405
411
|
gcp: firewallOptions.gcp,
|
|
406
412
|
});
|
|
413
|
+
// Zero-code session revocation / session-theft capture (config.sessions.guard / .capture).
|
|
414
|
+
require('./sessions').installHttpGuard(appConfig.resolveSessionsOptions());
|
|
407
415
|
}
|
|
408
416
|
} catch (e) {
|
|
409
417
|
console.error('[securenow] OTel start failed:', e && e.stack || e);
|