securenow 8.23.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 +730 -333
- package/README.md +91 -28
- package/SKILL-API.md +657 -530
- package/SKILL-CLI.md +293 -663
- package/app-config.js +90 -15
- package/cidr.js +139 -20
- package/cli/credentials.js +11 -0
- package/cli/init.js +121 -17
- package/cli/utils.js +26 -14
- package/cli.js +9 -6
- package/console-instrumentation.js +6 -0
- package/crash-capture.js +13 -1
- package/esm-detect.js +5 -1
- package/fastify-hooks.js +67 -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-auto-capture.d.ts +5 -2
- package/nextjs-auto-capture.js +77 -220
- package/nextjs-middleware.js +8 -4
- package/nextjs-wrapper.js +8 -4
- package/nextjs.d.ts +35 -67
- package/nextjs.js +75 -182
- package/nuxt-server-plugin.mjs +30 -478
- package/nuxt.mjs +15 -14
- package/package.json +8 -2
- package/pm2-env.js +47 -0
- package/register-vite.js +6 -2
- package/register.d.ts +27 -34
- package/request-capture.js +492 -0
- 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 +39 -447
package/nextjs-auto-capture.js
CHANGED
|
@@ -1,271 +1,129 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* SecureNow Next.js
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* export function register() {
|
|
13
|
-
* registerSecureNow();
|
|
14
|
-
* }
|
|
15
|
-
*
|
|
16
|
-
* That's it! Bodies are now captured automatically.
|
|
4
|
+
* SecureNow Next.js body capture, fallback layer.
|
|
5
|
+
*
|
|
6
|
+
* registerSecureNow() (securenow/nextjs) captures request headers and bodies at the HTTP layer,
|
|
7
|
+
* on the request's server span, like every other SecureNow integration. This module covers hosts
|
|
8
|
+
* where that layer never sees the request: it records the body when a route handler calls
|
|
9
|
+
* request.text() or request.json(). Once the HTTP layer has captured a request in this process,
|
|
10
|
+
* it leaves bodies to it.
|
|
11
|
+
*
|
|
12
|
+
* The generated instrumentation file imports it; importing it is all it needs.
|
|
17
13
|
*/
|
|
18
14
|
|
|
19
|
-
const { trace } = require('@opentelemetry/api');
|
|
20
15
|
const appConfig = require('./app-config');
|
|
16
|
+
const { DEFAULT_SENSITIVE_FIELDS, redactSensitiveData, redactBodyText } = require('./request-capture');
|
|
21
17
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
// Default sensitive fields to redact from request bodies.
|
|
25
|
-
// OTP/code aliases use exact normalized matching; other fields are matched
|
|
26
|
-
// substring-wise against lowercased keys (see redactSensitiveData),
|
|
27
|
-
// so e.g. 'card' also catches 'creditCard' and 'account' also catches
|
|
28
|
-
// 'accountNumber'. Over-redaction here is intentional: a falsely-redacted
|
|
29
|
-
// telemetry value is always safer than a leaked secret. Entries are kept
|
|
30
|
-
// specific enough to avoid nuking broad benign keys (e.g. we use
|
|
31
|
-
// 'firstname'/'lastname'/'fullname', never bare 'name').
|
|
32
|
-
const DEFAULT_SENSITIVE_FIELDS = [
|
|
33
|
-
...OTP_SENSITIVE_FIELDS,
|
|
34
|
-
// credentials / auth
|
|
35
|
-
'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
|
|
36
|
-
'access_token', 'auth', 'authorization', 'bearer', 'credentials',
|
|
37
|
-
'mysql_pwd', 'otp', 'mfa', 'totp', 'sessionid', 'session_id',
|
|
38
|
-
'cookie', 'set-cookie',
|
|
39
|
-
// financial
|
|
40
|
-
'stripeToken', 'card', 'cardnumber', 'ccv', 'cvc', 'cvv',
|
|
41
|
-
'iban', 'account', 'accountnumber', 'routing', 'sortcode', 'taxid',
|
|
42
|
-
// PII
|
|
43
|
-
'ssn', 'pin', 'email', 'e_mail', 'phone', 'mobile', 'dob', 'birthdate',
|
|
44
|
-
'firstname', 'lastname', 'fullname', 'address', 'postcode', 'zip',
|
|
45
|
-
'passport', 'license',
|
|
46
|
-
];
|
|
47
|
-
|
|
48
|
-
// Conservative value-shape redactors. Key-name matching misses secrets that
|
|
49
|
-
// land in free-form string values (GraphQL bodies, message fields, etc.), so
|
|
50
|
-
// as a second layer we scrub string VALUES that *look like* a secret/PII.
|
|
51
|
-
// These are intentionally precise/bounded so they don't garble normal prose,
|
|
52
|
-
// and they only ever transform captured telemetry strings (read-only) — never
|
|
53
|
-
// the actual request/response stream. Compiled once at module load.
|
|
54
|
-
const VALUE_REDACTORS = [
|
|
55
|
-
// JWT: three base64url segments. Anchored to the eyJ header so it won't
|
|
56
|
-
// match arbitrary dotted tokens.
|
|
57
|
-
{ name: 'jwt', re: /eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}/g },
|
|
58
|
-
// Bearer/Basic auth header value embedded in a body string.
|
|
59
|
-
{ name: 'bearer', re: /\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi },
|
|
60
|
-
// Stripe-style / SecureNow live+test API keys.
|
|
61
|
-
{ name: 'apikey', re: /\b(?:sk|pk|rk|snk)_(?:live|test)_[A-Za-z0-9]{8,}/g },
|
|
62
|
-
// Email addresses (bounded local/domain parts).
|
|
63
|
-
{ name: 'email', re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,24}\b/g },
|
|
64
|
-
// Credit-card-like: 13–19 digit runs that pass a Luhn check, allowing
|
|
65
|
-
// space/dash grouping. Luhn-gated to avoid clobbering ordinary long numbers.
|
|
66
|
-
{ name: 'card', re: /\b(?:\d[ -]?){13,19}\b/g, luhn: true },
|
|
67
|
-
];
|
|
68
|
-
|
|
69
|
-
// Skip the value-shape scan on very large strings to bound per-body cost.
|
|
70
|
-
const MAX_VALUE_SCAN_LENGTH = 16384;
|
|
18
|
+
const HTTP_CAPTURE_ACTIVE = Symbol.for('securenow.nextjs.httpCaptureActive');
|
|
19
|
+
const PATCHED = Symbol.for('securenow.nextjs.autoCapturePatched');
|
|
71
20
|
|
|
72
|
-
function
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (d < 0 || d > 9) return false;
|
|
78
|
-
if (alt) { d *= 2; if (d > 9) d -= 9; }
|
|
79
|
-
sum += d;
|
|
80
|
-
alt = !alt;
|
|
21
|
+
function activeSpan() {
|
|
22
|
+
try {
|
|
23
|
+
return require('@opentelemetry/api').trace.getActiveSpan() || null;
|
|
24
|
+
} catch (_) {
|
|
25
|
+
return null;
|
|
81
26
|
}
|
|
82
|
-
return sum % 10 === 0;
|
|
83
27
|
}
|
|
84
28
|
|
|
85
29
|
/**
|
|
86
|
-
*
|
|
87
|
-
* Returns the input unchanged when nothing matches (cheap common case).
|
|
30
|
+
* Check if body capture is enabled
|
|
88
31
|
*/
|
|
89
|
-
function
|
|
90
|
-
|
|
91
|
-
// Credential assignments in GraphQL/query strings need field-aware
|
|
92
|
-
// redaction even though a short numeric OTP has no unique value shape.
|
|
93
|
-
value = redactOtpQuery(redactOtpGraphQL(value));
|
|
94
|
-
if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound shape scans
|
|
95
|
-
let out = value;
|
|
96
|
-
for (const r of VALUE_REDACTORS) {
|
|
97
|
-
r.re.lastIndex = 0;
|
|
98
|
-
if (r.luhn) {
|
|
99
|
-
out = out.replace(r.re, (m) => {
|
|
100
|
-
const digits = m.replace(/[ -]/g, '');
|
|
101
|
-
if (digits.length < 13 || digits.length > 19) return m;
|
|
102
|
-
return luhnValid(digits) ? '[REDACTED]' : m;
|
|
103
|
-
});
|
|
104
|
-
} else {
|
|
105
|
-
out = out.replace(r.re, '[REDACTED]');
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return out;
|
|
32
|
+
function isBodyCaptureEnabled() {
|
|
33
|
+
return appConfig.boolConfig('capture.body', true);
|
|
109
34
|
}
|
|
110
35
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
function
|
|
115
|
-
if (!
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
} else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
|
|
124
|
-
redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
|
|
125
|
-
} else if (typeof redacted[key] === 'string') {
|
|
126
|
-
// Second layer: even if the key name looks benign, scrub values that
|
|
127
|
-
// *look like* a secret/PII (JWT, bearer token, API key, email, card).
|
|
128
|
-
redacted[key] = redactSensitiveValue(redacted[key]);
|
|
36
|
+
// Attach the redacted body to the span right away, while the handler that is awaiting its body
|
|
37
|
+
// still holds the span open. (Deferring this with setImmediate put it after the span had ended,
|
|
38
|
+
// and an ended span drops new attributes, so no body was ever recorded.)
|
|
39
|
+
function captureBodyText(span, bodyText, contentType) {
|
|
40
|
+
if (!span || typeof bodyText !== 'string') return;
|
|
41
|
+
try {
|
|
42
|
+
const maxBodySize = appConfig.numberConfig('capture.maxBodySize', 10240, 1024);
|
|
43
|
+
const sensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...appConfig.listConfig('capture.sensitiveFields')];
|
|
44
|
+
if (bodyText.length > maxBodySize) {
|
|
45
|
+
span.setAttribute('http.request.body', `[TOO LARGE: ${bodyText.length} bytes]`);
|
|
46
|
+
span.setAttribute('http.request.body.size', bodyText.length);
|
|
47
|
+
return;
|
|
129
48
|
}
|
|
49
|
+
let redacted;
|
|
50
|
+
try {
|
|
51
|
+
redacted = redactBodyText(bodyText, contentType, sensitiveFields);
|
|
52
|
+
} catch (_) {
|
|
53
|
+
span.setAttribute('http.request.body', '[UNPARSEABLE - REDACTED FOR SAFETY]');
|
|
54
|
+
span.setAttribute('http.request.body.parse_error', true);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (!redacted) return;
|
|
58
|
+
span.setAttributes({
|
|
59
|
+
'http.request.body': redacted.body.substring(0, maxBodySize),
|
|
60
|
+
'http.request.body.type': redacted.type,
|
|
61
|
+
'http.request.body.size': bodyText.length,
|
|
62
|
+
});
|
|
63
|
+
} catch (_) {
|
|
64
|
+
// Never break the request.
|
|
130
65
|
}
|
|
131
|
-
|
|
132
|
-
return redacted;
|
|
133
66
|
}
|
|
134
67
|
|
|
135
68
|
/**
|
|
136
|
-
*
|
|
69
|
+
* Capture from a Request whose body was already read through the patch, or can still be cloned.
|
|
137
70
|
*/
|
|
138
71
|
async function safeBodyCapture(request, span) {
|
|
139
|
-
if (!span) return;
|
|
140
|
-
|
|
72
|
+
if (!span || !request) return;
|
|
141
73
|
try {
|
|
142
|
-
const contentType = request.headers.get('content-type') || '';
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
146
|
-
|
|
147
|
-
// Only for supported types
|
|
148
|
-
if (!contentType.includes('application/json') &&
|
|
149
|
-
!contentType.includes('application/graphql') &&
|
|
150
|
-
!contentType.includes('application/x-www-form-urlencoded')) {
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Try to read from cache if available (Next.js may have already read it)
|
|
155
|
-
let bodyText;
|
|
156
|
-
|
|
157
|
-
// Attempt 1: Check if body was already cached by Next.js
|
|
158
|
-
if (request._bodyText) {
|
|
159
|
-
bodyText = request._bodyText;
|
|
160
|
-
} else {
|
|
161
|
-
// Attempt 2: Try to clone and read
|
|
74
|
+
const contentType = (request.headers && typeof request.headers.get === 'function' && request.headers.get('content-type')) || '';
|
|
75
|
+
let bodyText = request._bodyText;
|
|
76
|
+
if (bodyText === undefined) {
|
|
162
77
|
try {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
// Cache it for Next.js
|
|
166
|
-
request._bodyText = bodyText;
|
|
167
|
-
} catch (e) {
|
|
168
|
-
// If clone fails, body was already consumed - skip silently
|
|
78
|
+
bodyText = await request.clone().text();
|
|
79
|
+
} catch (_) {
|
|
169
80
|
return;
|
|
170
81
|
}
|
|
171
82
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
span.setAttribute('http.request.body', `[TOO LARGE: ${bodyText.length} bytes]`);
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Parse and redact
|
|
179
|
-
if (contentType.includes('application/json') || contentType.includes('application/graphql')) {
|
|
180
|
-
try {
|
|
181
|
-
const parsed = JSON.parse(bodyText);
|
|
182
|
-
const redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
183
|
-
span.setAttributes({
|
|
184
|
-
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
185
|
-
'http.request.body.type': contentType.includes('graphql') ? 'graphql' : 'json',
|
|
186
|
-
'http.request.body.size': bodyText.length,
|
|
187
|
-
});
|
|
188
|
-
} catch (e) {
|
|
189
|
-
// Parse error - skip
|
|
190
|
-
}
|
|
191
|
-
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
192
|
-
try {
|
|
193
|
-
const params = new URLSearchParams(bodyText);
|
|
194
|
-
const parsed = Object.fromEntries(params);
|
|
195
|
-
const redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
196
|
-
span.setAttributes({
|
|
197
|
-
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
198
|
-
'http.request.body.type': 'form',
|
|
199
|
-
'http.request.body.size': bodyText.length,
|
|
200
|
-
});
|
|
201
|
-
} catch (e) {
|
|
202
|
-
// Parse error - skip
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
} catch (error) {
|
|
83
|
+
captureBodyText(span, bodyText, contentType);
|
|
84
|
+
} catch (_) {
|
|
206
85
|
// Silently fail - never break the request
|
|
207
86
|
}
|
|
208
87
|
}
|
|
209
88
|
|
|
210
89
|
/**
|
|
211
|
-
*
|
|
212
|
-
*/
|
|
213
|
-
function isBodyCaptureEnabled() {
|
|
214
|
-
return appConfig.boolConfig('capture.body', true);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Patch Next.js Request to cache body text
|
|
219
|
-
* This allows us to read the body without consuming it
|
|
90
|
+
* Patch Request.text()/json() so the body a route handler reads is recorded on its span.
|
|
220
91
|
*/
|
|
221
92
|
function patchNextRequest() {
|
|
222
|
-
if (typeof Request === 'undefined') return;
|
|
223
|
-
|
|
93
|
+
if (typeof Request === 'undefined' || Request.prototype[PATCHED]) return;
|
|
94
|
+
|
|
224
95
|
const originalText = Request.prototype.text;
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
// Patch text() to cache result
|
|
228
|
-
Request.prototype.text = async function() {
|
|
96
|
+
|
|
97
|
+
Request.prototype.text = async function securenowText() {
|
|
229
98
|
if (this._bodyText !== undefined) {
|
|
230
99
|
return this._bodyText;
|
|
231
100
|
}
|
|
101
|
+
const span = activeSpan();
|
|
232
102
|
const text = await originalText.call(this);
|
|
233
103
|
this._bodyText = text;
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (span) {
|
|
239
|
-
// Schedule capture after this call (non-blocking)
|
|
240
|
-
setImmediate(() => {
|
|
241
|
-
safeBodyCapture(this, span).catch(() => {});
|
|
242
|
-
});
|
|
243
|
-
}
|
|
104
|
+
|
|
105
|
+
if (!globalThis[HTTP_CAPTURE_ACTIVE] && isBodyCaptureEnabled() && ['POST', 'PUT', 'PATCH'].includes(this.method)) {
|
|
106
|
+
const contentType = (this.headers && typeof this.headers.get === 'function' && this.headers.get('content-type')) || '';
|
|
107
|
+
captureBodyText(span, text, contentType);
|
|
244
108
|
}
|
|
245
|
-
|
|
109
|
+
|
|
246
110
|
return text;
|
|
247
111
|
};
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
// First get text
|
|
252
|
-
const text = await this.text();
|
|
253
|
-
// Then parse
|
|
254
|
-
return JSON.parse(text);
|
|
112
|
+
|
|
113
|
+
Request.prototype.json = async function securenowJson() {
|
|
114
|
+
return JSON.parse(await this.text());
|
|
255
115
|
};
|
|
256
|
-
|
|
257
|
-
|
|
116
|
+
|
|
117
|
+
Object.defineProperty(Request.prototype, PATCHED, { value: true });
|
|
118
|
+
console.log('[securenow] Next.js body capture fallback installed (request.text() / request.json())');
|
|
258
119
|
}
|
|
259
120
|
|
|
260
121
|
// Auto-patch when module is imported
|
|
261
122
|
if (isBodyCaptureEnabled()) {
|
|
262
123
|
try {
|
|
263
124
|
patchNextRequest();
|
|
264
|
-
console.log('[securenow] 📝 Automatic body capture: ENABLED');
|
|
265
|
-
console.log('[securenow] 💡 No code changes needed - bodies captured automatically!');
|
|
266
125
|
} catch (error) {
|
|
267
|
-
console.warn('[securenow]
|
|
268
|
-
console.warn('[securenow] 💡 Body capture disabled. Use manual approach if needed.');
|
|
126
|
+
console.warn('[securenow] Next.js body capture fallback not installed:', error && error.message);
|
|
269
127
|
}
|
|
270
128
|
} else {
|
|
271
129
|
console.log('[securenow] Automatic body capture: DISABLED (config.capture.body=false)');
|
|
@@ -277,4 +135,3 @@ module.exports = {
|
|
|
277
135
|
redactSensitiveData,
|
|
278
136
|
isBodyCaptureEnabled,
|
|
279
137
|
};
|
|
280
|
-
|
package/nextjs-middleware.js
CHANGED
|
@@ -51,18 +51,21 @@ const DEFAULT_SENSITIVE_FIELDS = [
|
|
|
51
51
|
// and they only ever transform captured telemetry strings (read-only) — never
|
|
52
52
|
// the actual request/response stream. Compiled once at module load.
|
|
53
53
|
const VALUE_REDACTORS = [
|
|
54
|
+
// The JWT and email patterns only start where a run of their characters starts (the
|
|
55
|
+
// lookbehind), so a long run that holds no match is scanned once, not once per position:
|
|
56
|
+
// a crafted 10KB value costs microseconds, not 50ms+ of event loop.
|
|
54
57
|
// JWT: three base64url segments. Anchored to the eyJ header so it won't
|
|
55
58
|
// match arbitrary dotted tokens.
|
|
56
|
-
{ name: 'jwt', re: /eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}/g },
|
|
59
|
+
{ 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 },
|
|
57
60
|
// Bearer/Basic auth header value embedded in a body string.
|
|
58
61
|
{ name: 'bearer', re: /\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi },
|
|
59
62
|
// Stripe-style / SecureNow live+test API keys.
|
|
60
63
|
{ name: 'apikey', re: /\b(?:sk|pk|rk|snk)_(?:live|test)_[A-Za-z0-9]{8,}/g },
|
|
61
|
-
// Email addresses (bounded local/domain parts).
|
|
62
|
-
{ name: 'email', re:
|
|
64
|
+
// Email addresses (bounded local/domain parts: 64 and 253 characters at most).
|
|
65
|
+
{ name: 'email', hint: '@', re: /(?<![\w.%+-])[\w.%+-]{1,64}@[A-Za-z0-9.-]{1,253}\.[A-Za-z]{2,24}\b/g },
|
|
63
66
|
// Credit-card-like: 13–19 digit runs that pass a Luhn check, allowing
|
|
64
67
|
// space/dash grouping. Luhn-gated to avoid clobbering ordinary long numbers.
|
|
65
|
-
{ name: 'card', re: /\b(?:\d[ -]?){
|
|
68
|
+
{ name: 'card', re: /\b(?:\d[ -]?){12,18}\d\b/g, luhn: true },
|
|
66
69
|
];
|
|
67
70
|
|
|
68
71
|
// Skip the value-shape scan on very large strings to bound per-body cost.
|
|
@@ -93,6 +96,7 @@ function redactSensitiveValue(value) {
|
|
|
93
96
|
if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound shape scans
|
|
94
97
|
let out = value;
|
|
95
98
|
for (const r of VALUE_REDACTORS) {
|
|
99
|
+
if (r.hint && !out.includes(r.hint)) continue;
|
|
96
100
|
r.re.lastIndex = 0;
|
|
97
101
|
if (r.luhn) {
|
|
98
102
|
out = out.replace(r.re, (m) => {
|
package/nextjs-wrapper.js
CHANGED
|
@@ -51,18 +51,21 @@ const DEFAULT_SENSITIVE_FIELDS = [
|
|
|
51
51
|
// and they only ever transform captured telemetry strings (read-only) — never
|
|
52
52
|
// the actual request/response stream. Compiled once at module load.
|
|
53
53
|
const VALUE_REDACTORS = [
|
|
54
|
+
// The JWT and email patterns only start where a run of their characters starts (the
|
|
55
|
+
// lookbehind), so a long run that holds no match is scanned once, not once per position:
|
|
56
|
+
// a crafted 10KB value costs microseconds, not 50ms+ of event loop.
|
|
54
57
|
// JWT: three base64url segments. Anchored to the eyJ header so it won't
|
|
55
58
|
// match arbitrary dotted tokens.
|
|
56
|
-
{ name: 'jwt', re: /eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}/g },
|
|
59
|
+
{ 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 },
|
|
57
60
|
// Bearer/Basic auth header value embedded in a body string.
|
|
58
61
|
{ name: 'bearer', re: /\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi },
|
|
59
62
|
// Stripe-style / SecureNow live+test API keys.
|
|
60
63
|
{ name: 'apikey', re: /\b(?:sk|pk|rk|snk)_(?:live|test)_[A-Za-z0-9]{8,}/g },
|
|
61
|
-
// Email addresses (bounded local/domain parts).
|
|
62
|
-
{ name: 'email', re:
|
|
64
|
+
// Email addresses (bounded local/domain parts: 64 and 253 characters at most).
|
|
65
|
+
{ name: 'email', hint: '@', re: /(?<![\w.%+-])[\w.%+-]{1,64}@[A-Za-z0-9.-]{1,253}\.[A-Za-z]{2,24}\b/g },
|
|
63
66
|
// Credit-card-like: 13–19 digit runs that pass a Luhn check, allowing
|
|
64
67
|
// space/dash grouping. Luhn-gated to avoid clobbering ordinary long numbers.
|
|
65
|
-
{ name: 'card', re: /\b(?:\d[ -]?){
|
|
68
|
+
{ name: 'card', re: /\b(?:\d[ -]?){12,18}\d\b/g, luhn: true },
|
|
66
69
|
];
|
|
67
70
|
|
|
68
71
|
// Skip the value-shape scan on very large strings to bound per-body cost.
|
|
@@ -93,6 +96,7 @@ function redactSensitiveValue(value) {
|
|
|
93
96
|
if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound shape scans
|
|
94
97
|
let out = value;
|
|
95
98
|
for (const r of VALUE_REDACTORS) {
|
|
99
|
+
if (r.hint && !out.includes(r.hint)) continue;
|
|
96
100
|
r.re.lastIndex = 0;
|
|
97
101
|
if (r.luhn) {
|
|
98
102
|
out = out.replace(r.re, (m) => {
|
package/nextjs.d.ts
CHANGED
|
@@ -1,102 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SecureNow Next.js Integration TypeScript Declarations
|
|
3
|
+
*
|
|
4
|
+
* Settings live in .securenow/runtime.json (or SECURENOW_CREDENTIALS on hosts without file
|
|
5
|
+
* mounts); the options below only override them.
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
8
|
export interface RegisterOptions {
|
|
6
9
|
/**
|
|
7
|
-
* Service name for OpenTelemetry traces
|
|
8
|
-
*
|
|
10
|
+
* Service name for OpenTelemetry traces.
|
|
11
|
+
* Leave unset: SecureNow routes telemetry by the app key, and another name hides the data
|
|
12
|
+
* from the app's dashboard. Only for a self-hosted collector.
|
|
13
|
+
* @default the app key from .securenow/runtime.json
|
|
9
14
|
*/
|
|
10
15
|
serviceName?: string;
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
|
-
* OTLP endpoint for
|
|
18
|
+
* Advanced OTLP endpoint override, for a self-hosted collector. Leave unset for SecureNow.
|
|
14
19
|
* @default https://ingest.securenow.ai
|
|
15
|
-
*
|
|
16
|
-
* Advanced OTLP endpoint override. Normal SecureNow apps should leave this
|
|
17
|
-
* unset so the ingest gateway can route by app.key to the dashboard-selected
|
|
18
|
-
* instance.
|
|
19
20
|
*/
|
|
20
21
|
endpoint?: string;
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Deployment environment sent as OpenTelemetry deployment.environment.
|
|
24
|
-
* @default .
|
|
25
|
+
* @default config.runtime.deploymentEnvironment from .securenow/runtime.json
|
|
25
26
|
*/
|
|
26
27
|
environment?: string;
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
|
-
* Don't append UUID to service name
|
|
30
|
-
* @default
|
|
30
|
+
* Don't append a UUID to the service name.
|
|
31
|
+
* @default true when an app key is configured
|
|
31
32
|
*/
|
|
32
33
|
noUuid?: boolean;
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
*
|
|
36
|
-
* @default
|
|
35
|
+
/**
|
|
36
|
+
* Capture request bodies (redacted, size-capped, multipart as metadata only).
|
|
37
|
+
* @default config.capture.body from .securenow/runtime.json (true)
|
|
37
38
|
*/
|
|
38
39
|
captureBody?: boolean;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
|
-
* Register SecureNow
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* @example
|
|
47
|
-
* ```typescript
|
|
48
|
-
* // instrumentation.ts
|
|
49
|
-
* import { createRequire } from 'node:module';
|
|
50
|
-
*
|
|
51
|
-
* const require = createRequire(import.meta.url);
|
|
43
|
+
* Register SecureNow (tracing, logs, body capture, firewall, sessions) for the Next.js Node.js runtime.
|
|
44
|
+
* `npx securenow init` writes this file for you.
|
|
45
|
+
*
|
|
46
|
+
* @param options - Optional overrides
|
|
52
47
|
*
|
|
53
|
-
* export async function register() {
|
|
54
|
-
* if (process.env.NEXT_RUNTIME !== 'nodejs') return;
|
|
55
|
-
* const { registerSecureNow } = require('securenow/nextjs');
|
|
56
|
-
* registerSecureNow({ captureBody: true });
|
|
57
|
-
* require('securenow/nextjs-auto-capture');
|
|
58
|
-
* }
|
|
59
|
-
* ```
|
|
60
|
-
*
|
|
61
48
|
* @example
|
|
62
49
|
* ```typescript
|
|
63
|
-
* //
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
50
|
+
* // instrumentation.ts (what `npx securenow init` writes)
|
|
51
|
+
* export async function register() {
|
|
52
|
+
* if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
53
|
+
* try {
|
|
54
|
+
* const securenowNext = await import('securenow/nextjs');
|
|
55
|
+
* const registerSecureNow = securenowNext.registerSecureNow || securenowNext.default?.registerSecureNow;
|
|
56
|
+
* registerSecureNow({ captureBody: true });
|
|
57
|
+
* await import('securenow/nextjs-auto-capture');
|
|
58
|
+
* } catch (error) {
|
|
59
|
+
* console.error('[securenow] SecureNow did not start; the app is running without it:', error);
|
|
60
|
+
* }
|
|
61
|
+
* }
|
|
72
62
|
* }
|
|
73
63
|
* ```
|
|
64
|
+
*
|
|
65
|
+
* next.config: `serverExternalPackages: ['securenow']` and
|
|
66
|
+
* `outputFileTracingIncludes: { '/*': ['./node_modules/securenow/**\/*'] }` (Next.js 15+);
|
|
67
|
+
* Next.js 14 uses `experimental.instrumentationHook`, `experimental.serverComponentsExternalPackages`
|
|
68
|
+
* and `experimental.outputFileTracingIncludes`.
|
|
74
69
|
*/
|
|
75
70
|
export function registerSecureNow(options?: RegisterOptions): void;
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Default sensitive fields that are automatically redacted from traces
|
|
79
|
-
*/
|
|
80
|
-
export const DEFAULT_SENSITIVE_FIELDS: readonly string[];
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Redact sensitive fields from an object
|
|
84
|
-
* @param obj - Object to redact
|
|
85
|
-
* @param sensitiveFields - Array of field names to redact (case-insensitive substring match)
|
|
86
|
-
* @returns Redacted copy of the object
|
|
87
|
-
*/
|
|
88
|
-
export function redactSensitiveData<T = any>(
|
|
89
|
-
obj: T,
|
|
90
|
-
sensitiveFields?: string[]
|
|
91
|
-
): T;
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Redact sensitive data from GraphQL query strings
|
|
95
|
-
* @param query - GraphQL query string
|
|
96
|
-
* @param sensitiveFields - Array of field names to redact
|
|
97
|
-
* @returns Redacted query string
|
|
98
|
-
*/
|
|
99
|
-
export function redactGraphQLQuery(
|
|
100
|
-
query: string,
|
|
101
|
-
sensitiveFields?: string[]
|
|
102
|
-
): string;
|