securenow 6.0.2 → 6.1.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/CONSUMING-APPS-GUIDE.md +455 -0
- package/NPM_README.md +2029 -0
- package/README.md +297 -40
- package/SKILL-API.md +634 -0
- package/SKILL-CLI.md +454 -0
- package/cidr.js +83 -0
- package/cli/apps.js +585 -0
- package/cli/auth.js +280 -0
- package/cli/client.js +115 -0
- package/cli/config.js +173 -0
- package/cli/diagnostics.js +387 -0
- package/cli/firewall.js +100 -0
- package/cli/fp.js +638 -0
- package/cli/init.js +201 -0
- package/cli/monitor.js +440 -0
- package/cli/run.js +148 -0
- package/cli/security.js +980 -0
- package/cli/ui.js +386 -0
- package/cli/utils.js +127 -0
- package/cli.js +466 -455
- package/console-instrumentation.js +147 -136
- package/docs/ALL-FRAMEWORKS-QUICKSTART.md +1377 -455
- package/docs/API-KEYS-GUIDE.md +233 -0
- package/docs/ARCHITECTURE.md +3 -3
- package/docs/AUTO-BODY-CAPTURE.md +1 -1
- package/docs/AUTO-SETUP-SUMMARY.md +331 -0
- package/docs/AUTO-SETUP.md +4 -4
- package/docs/AUTOMATIC-IP-CAPTURE.md +5 -5
- package/docs/BODY-CAPTURE-FIX.md +261 -0
- package/docs/BODY-CAPTURE-QUICKSTART.md +2 -2
- package/docs/CHANGELOG-NEXTJS.md +1 -35
- package/docs/COMPLETION-REPORT.md +408 -0
- package/docs/CUSTOMER-GUIDE.md +16 -16
- package/docs/EASIEST-SETUP.md +5 -5
- package/docs/ENVIRONMENT-VARIABLES.md +880 -652
- package/docs/EXPRESS-BODY-CAPTURE.md +13 -12
- package/docs/EXPRESS-SETUP-GUIDE.md +719 -720
- package/docs/FINAL-SOLUTION.md +335 -0
- package/docs/FIREWALL-GUIDE.md +426 -0
- package/docs/IMPLEMENTATION-SUMMARY.md +410 -0
- package/docs/INDEX.md +22 -4
- package/docs/LOGGING-GUIDE.md +701 -708
- package/docs/LOGGING-QUICKSTART.md +234 -255
- package/docs/NEXTJS-BODY-CAPTURE-COMPARISON.md +323 -0
- package/docs/NEXTJS-BODY-CAPTURE.md +2 -2
- package/docs/NEXTJS-GUIDE.md +14 -14
- package/docs/NEXTJS-QUICKSTART.md +1 -1
- package/docs/NEXTJS-SETUP-COMPLETE.md +795 -0
- package/docs/NEXTJS-WRAPPER-APPROACH.md +1 -1
- package/docs/NUXT-GUIDE.md +166 -0
- package/docs/QUICKSTART-BODY-CAPTURE.md +2 -2
- package/docs/REDACTION-EXAMPLES.md +1 -1
- package/docs/REQUEST-BODY-CAPTURE.md +19 -10
- package/docs/SOLUTION-SUMMARY.md +312 -0
- package/docs/VERCEL-OTEL-MIGRATION.md +3 -3
- package/examples/README.md +6 -6
- package/examples/instrumentation-with-auto-capture.ts +1 -1
- package/examples/nextjs-env-example.txt +2 -2
- package/examples/nextjs-instrumentation.js +1 -1
- package/examples/nextjs-instrumentation.ts +1 -1
- package/examples/nextjs-with-logging-example.md +6 -6
- package/examples/nextjs-with-options.ts +1 -1
- package/examples/test-nextjs-setup.js +1 -1
- package/firewall-cloud.js +212 -0
- package/firewall-iptables.js +139 -0
- package/firewall-only.js +38 -0
- package/firewall-tcp.js +74 -0
- package/firewall.js +720 -0
- package/free-trial-banner.js +174 -0
- package/nextjs-auto-capture.js +199 -207
- package/nextjs-middleware.js +186 -181
- package/nextjs-webpack-config.js +88 -53
- package/nextjs-wrapper.js +158 -158
- package/nextjs.d.ts +1 -1
- package/nextjs.js +639 -647
- package/nuxt-server-plugin.mjs +423 -0
- package/nuxt.d.ts +60 -0
- package/nuxt.mjs +75 -0
- package/package.json +186 -164
- package/postinstall.js +6 -6
- package/register.d.ts +1 -1
- package/register.js +39 -4
- package/resolve-ip.js +77 -0
- package/tracing.d.ts +2 -1
- package/tracing.js +295 -34
- package/web-vite.mjs +239 -156
- package/LICENSE +0 -15
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Free Trial Banner — auto-injects a visible "Testing Environment" banner
|
|
5
|
+
* into HTML pages served by apps using the SecureNow free trial instance.
|
|
6
|
+
*
|
|
7
|
+
* Opt-out: set SECURENOW_HIDE_BANNER=1
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const FREETRIAL_HOST = 'freetrial.securenow.ai';
|
|
11
|
+
|
|
12
|
+
function isFreeTrial(endpointBase) {
|
|
13
|
+
return !!endpointBase && endpointBase.includes(FREETRIAL_HOST);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* istanbul ignore next — runs in browser, not Node */
|
|
17
|
+
function _bannerClientCode() {
|
|
18
|
+
if (window.__snBanner) return;
|
|
19
|
+
window.__snBanner = 1;
|
|
20
|
+
|
|
21
|
+
function create() {
|
|
22
|
+
if (document.getElementById('sn-ft-banner')) return;
|
|
23
|
+
|
|
24
|
+
var d = document.createElement('div');
|
|
25
|
+
d.id = 'sn-ft-banner';
|
|
26
|
+
d.style.cssText =
|
|
27
|
+
'position:fixed;top:0;left:0;right:0;z-index:2147483647;' +
|
|
28
|
+
'background:#FEF3CD;color:#856404;padding:10px 16px;' +
|
|
29
|
+
'font-family:system-ui,-apple-system,sans-serif;font-size:13px;' +
|
|
30
|
+
'text-align:center;border-bottom:2px solid #FFE69C;' +
|
|
31
|
+
'display:flex;align-items:center;justify-content:center;gap:6px;' +
|
|
32
|
+
'box-shadow:0 2px 8px rgba(0,0,0,0.12)';
|
|
33
|
+
|
|
34
|
+
var icon = document.createElement('span');
|
|
35
|
+
icon.textContent = '\u26a0\ufe0f';
|
|
36
|
+
d.appendChild(icon);
|
|
37
|
+
|
|
38
|
+
var msg = document.createElement('span');
|
|
39
|
+
var strong = document.createElement('strong');
|
|
40
|
+
strong.textContent = 'Testing Environment:';
|
|
41
|
+
msg.appendChild(strong);
|
|
42
|
+
msg.appendChild(document.createTextNode(
|
|
43
|
+
' Only add test applications. For production usage, '
|
|
44
|
+
));
|
|
45
|
+
|
|
46
|
+
var link = document.createElement('a');
|
|
47
|
+
link.href = 'https://app.securenow.ai/dashboard/settings/instances';
|
|
48
|
+
link.target = '_blank';
|
|
49
|
+
link.rel = 'noopener';
|
|
50
|
+
link.style.cssText = 'color:#664D03;font-weight:600;text-decoration:underline';
|
|
51
|
+
link.textContent = 'create a new production instance';
|
|
52
|
+
msg.appendChild(link);
|
|
53
|
+
msg.appendChild(document.createTextNode('.'));
|
|
54
|
+
d.appendChild(msg);
|
|
55
|
+
|
|
56
|
+
var upgradeBtn = document.createElement('a');
|
|
57
|
+
upgradeBtn.href = 'https://app.securenow.ai/dashboard/settings/instances';
|
|
58
|
+
upgradeBtn.target = '_blank';
|
|
59
|
+
upgradeBtn.rel = 'noopener';
|
|
60
|
+
upgradeBtn.textContent = '\u26a1 Upgrade';
|
|
61
|
+
upgradeBtn.style.cssText =
|
|
62
|
+
'display:inline-flex;align-items:center;gap:4px;' +
|
|
63
|
+
'background:#D97706;color:#fff;padding:4px 12px;border-radius:4px;' +
|
|
64
|
+
'font-size:12px;font-weight:600;text-decoration:none;margin-left:10px;' +
|
|
65
|
+
'white-space:nowrap;transition:background 0.15s';
|
|
66
|
+
upgradeBtn.onmouseover = function () { upgradeBtn.style.background = '#B45309'; };
|
|
67
|
+
upgradeBtn.onmouseout = function () { upgradeBtn.style.background = '#D97706'; };
|
|
68
|
+
d.appendChild(upgradeBtn);
|
|
69
|
+
|
|
70
|
+
var close = document.createElement('button');
|
|
71
|
+
close.textContent = '\u00d7';
|
|
72
|
+
close.style.cssText =
|
|
73
|
+
'background:none;border:none;color:#856404;font-size:18px;' +
|
|
74
|
+
'cursor:pointer;margin-left:12px;padding:0 4px;line-height:1';
|
|
75
|
+
close.onclick = function () { d.style.display = 'none'; };
|
|
76
|
+
d.appendChild(close);
|
|
77
|
+
|
|
78
|
+
document.body.prepend(d);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (document.readyState === 'loading') {
|
|
82
|
+
document.addEventListener('DOMContentLoaded', create);
|
|
83
|
+
} else {
|
|
84
|
+
create();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
var BANNER_SCRIPT =
|
|
89
|
+
'<script data-securenow-banner>(' +
|
|
90
|
+
_bannerClientCode.toString() +
|
|
91
|
+
')()</scr' + 'ipt>';
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Monkey-patch http.ServerResponse to inject the banner script into HTML
|
|
95
|
+
* responses. Searches for `<head...>` and inserts the script right after it.
|
|
96
|
+
* Skips compressed responses and non-HTML content types.
|
|
97
|
+
*/
|
|
98
|
+
function patchHttpForBanner() {
|
|
99
|
+
try {
|
|
100
|
+
var http = require('http');
|
|
101
|
+
var _origWrite = http.ServerResponse.prototype.write;
|
|
102
|
+
var _origEnd = http.ServerResponse.prototype.end;
|
|
103
|
+
|
|
104
|
+
function maybeInject(res, chunk) {
|
|
105
|
+
if (res._snBannerDone || !chunk) return chunk;
|
|
106
|
+
|
|
107
|
+
if (res._snIsHtml === undefined) {
|
|
108
|
+
var ct = res.getHeader('content-type');
|
|
109
|
+
var ce = res.getHeader('content-encoding');
|
|
110
|
+
var csp = res.getHeader('content-security-policy');
|
|
111
|
+
res._snIsHtml = !!(ct && String(ct).includes('text/html') && !ce && !csp);
|
|
112
|
+
}
|
|
113
|
+
if (!res._snIsHtml) {
|
|
114
|
+
res._snBannerDone = true;
|
|
115
|
+
return chunk;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
var isStr = typeof chunk === 'string';
|
|
119
|
+
var isBuf = Buffer.isBuffer(chunk);
|
|
120
|
+
if (!isStr && !isBuf) return chunk;
|
|
121
|
+
|
|
122
|
+
var str = isStr ? chunk : chunk.toString('utf8');
|
|
123
|
+
var headIdx = str.indexOf('<head');
|
|
124
|
+
if (headIdx === -1) return chunk;
|
|
125
|
+
|
|
126
|
+
var gt = str.indexOf('>', headIdx);
|
|
127
|
+
if (gt === -1) return chunk;
|
|
128
|
+
|
|
129
|
+
res._snBannerDone = true;
|
|
130
|
+
var result = str.slice(0, gt + 1) + BANNER_SCRIPT + str.slice(gt + 1);
|
|
131
|
+
return isStr ? result : Buffer.from(result, 'utf8');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
http.ServerResponse.prototype.write = function (chunk, encoding, cb) {
|
|
135
|
+
try {
|
|
136
|
+
var modified = maybeInject(this, chunk);
|
|
137
|
+
if (modified !== chunk) {
|
|
138
|
+
var enc = typeof encoding === 'function' ? 'utf8' : encoding;
|
|
139
|
+
var callback = typeof encoding === 'function' ? encoding : cb;
|
|
140
|
+
try {
|
|
141
|
+
if (this.getHeader('content-length')) {
|
|
142
|
+
this.setHeader('content-length', Buffer.byteLength(modified));
|
|
143
|
+
}
|
|
144
|
+
} catch (_) { /* headers already sent */ }
|
|
145
|
+
return _origWrite.call(this, modified, enc, callback);
|
|
146
|
+
}
|
|
147
|
+
} catch (_) { /* never break the app */ }
|
|
148
|
+
return _origWrite.call(this, chunk, encoding, cb);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
http.ServerResponse.prototype.end = function (chunk, encoding, cb) {
|
|
152
|
+
try {
|
|
153
|
+
var modified = maybeInject(this, chunk);
|
|
154
|
+
if (modified !== chunk) {
|
|
155
|
+
var enc = typeof encoding === 'function' ? 'utf8' : encoding;
|
|
156
|
+
var callback = typeof encoding === 'function' ? encoding : cb;
|
|
157
|
+
try {
|
|
158
|
+
if (this.getHeader('content-length')) {
|
|
159
|
+
this.setHeader('content-length', Buffer.byteLength(modified));
|
|
160
|
+
}
|
|
161
|
+
} catch (_) { /* headers already sent */ }
|
|
162
|
+
return _origEnd.call(this, modified, enc, callback);
|
|
163
|
+
}
|
|
164
|
+
} catch (_) { /* never break the app */ }
|
|
165
|
+
return _origEnd.call(this, chunk, encoding, cb);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
console.log('[securenow] Free trial banner injection enabled');
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.warn('[securenow] Could not setup free trial banner:', err.message);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = { isFreeTrial, patchHttpForBanner, BANNER_SCRIPT };
|
package/nextjs-auto-capture.js
CHANGED
|
@@ -1,207 +1,199 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SecureNow Next.js Automatic Body Capture
|
|
3
|
-
*
|
|
4
|
-
* This module automatically patches Next.js request handling to capture bodies
|
|
5
|
-
* WITHOUT requiring customers to wrap their handlers or change their code.
|
|
6
|
-
*
|
|
7
|
-
* Usage in instrumentation.ts:
|
|
8
|
-
*
|
|
9
|
-
* import { registerSecureNow } from 'securenow/nextjs';
|
|
10
|
-
* import 'securenow/nextjs-auto-capture'; // Just import this line!
|
|
11
|
-
*
|
|
12
|
-
* export function register() {
|
|
13
|
-
* registerSecureNow();
|
|
14
|
-
* }
|
|
15
|
-
*
|
|
16
|
-
* That's it! Bodies are now captured automatically.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
const { trace } = require('@opentelemetry/api');
|
|
20
|
-
|
|
21
|
-
// Default sensitive fields to redact
|
|
22
|
-
const DEFAULT_SENSITIVE_FIELDS = [
|
|
23
|
-
'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
|
|
24
|
-
'access_token', 'auth', 'credentials', 'mysql_pwd', 'stripeToken',
|
|
25
|
-
'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Redact sensitive fields from an object
|
|
30
|
-
*/
|
|
31
|
-
function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
32
|
-
if (!obj || typeof obj !== 'object') return obj;
|
|
33
|
-
|
|
34
|
-
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
35
|
-
|
|
36
|
-
for (const key
|
|
37
|
-
const lowerKey = key.toLowerCase();
|
|
38
|
-
|
|
39
|
-
if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
|
|
40
|
-
redacted[key] = '[REDACTED]';
|
|
41
|
-
} else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
|
|
42
|
-
redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return redacted;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Safe body capture that doesn't interfere with Next.js
|
|
51
|
-
*/
|
|
52
|
-
async function safeBodyCapture(request, span) {
|
|
53
|
-
if (!span) return;
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
const contentType = request.headers.get('content-type') || '';
|
|
57
|
-
const maxBodySize = parseInt(process.env.SECURENOW_MAX_BODY_SIZE ||
|
|
58
|
-
const customSensitiveFields = (process.env.SECURENOW_SENSITIVE_FIELDS || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
59
|
-
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
60
|
-
|
|
61
|
-
// Only for supported types
|
|
62
|
-
if (!contentType.includes('application/json') &&
|
|
63
|
-
!contentType.includes('application/graphql') &&
|
|
64
|
-
!contentType.includes('application/x-www-form-urlencoded')) {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Try to read from cache if available (Next.js may have already read it)
|
|
69
|
-
let bodyText;
|
|
70
|
-
|
|
71
|
-
// Attempt 1: Check if body was already cached by Next.js
|
|
72
|
-
if (request._bodyText) {
|
|
73
|
-
bodyText = request._bodyText;
|
|
74
|
-
} else {
|
|
75
|
-
// Attempt 2: Try to clone and read
|
|
76
|
-
try {
|
|
77
|
-
const cloned = request.clone();
|
|
78
|
-
bodyText = await cloned.text();
|
|
79
|
-
// Cache it for Next.js
|
|
80
|
-
request._bodyText = bodyText;
|
|
81
|
-
} catch (e) {
|
|
82
|
-
// If clone fails, body was already consumed - skip silently
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (bodyText.length > maxBodySize) {
|
|
88
|
-
span.setAttribute('http.request.body', `[TOO LARGE: ${bodyText.length} bytes]`);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Parse and redact
|
|
93
|
-
if (contentType.includes('application/json') || contentType.includes('application/graphql')) {
|
|
94
|
-
try {
|
|
95
|
-
const parsed = JSON.parse(bodyText);
|
|
96
|
-
const redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
97
|
-
span.setAttributes({
|
|
98
|
-
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
99
|
-
'http.request.body.type': contentType.includes('graphql') ? 'graphql' : 'json',
|
|
100
|
-
'http.request.body.size': bodyText.length,
|
|
101
|
-
});
|
|
102
|
-
} catch (e) {
|
|
103
|
-
// Parse error - skip
|
|
104
|
-
}
|
|
105
|
-
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
106
|
-
try {
|
|
107
|
-
const params = new URLSearchParams(bodyText);
|
|
108
|
-
const parsed = Object.fromEntries(params);
|
|
109
|
-
const redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
110
|
-
span.setAttributes({
|
|
111
|
-
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
112
|
-
'http.request.body.type': 'form',
|
|
113
|
-
'http.request.body.size': bodyText.length,
|
|
114
|
-
});
|
|
115
|
-
} catch (e) {
|
|
116
|
-
// Parse error - skip
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
} catch (error) {
|
|
120
|
-
// Silently fail - never break the request
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Check if body capture is enabled
|
|
126
|
-
*/
|
|
127
|
-
function isBodyCaptureEnabled() {
|
|
128
|
-
const enabled = String(process.env.SECURENOW_CAPTURE_BODY) === '1' ||
|
|
129
|
-
String(process.env.SECURENOW_CAPTURE_BODY).toLowerCase() === 'true';
|
|
130
|
-
return enabled;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Patch Next.js Request to cache body text
|
|
135
|
-
* This allows us to read the body without consuming it
|
|
136
|
-
*/
|
|
137
|
-
function patchNextRequest() {
|
|
138
|
-
if (typeof Request === 'undefined') return;
|
|
139
|
-
|
|
140
|
-
const originalText = Request.prototype.text;
|
|
141
|
-
const originalJson = Request.prototype.json;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
safeBodyCapture,
|
|
201
|
-
redactSensitiveData,
|
|
202
|
-
isBodyCaptureEnabled,
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
1
|
+
/**
|
|
2
|
+
* SecureNow Next.js Automatic Body Capture
|
|
3
|
+
*
|
|
4
|
+
* This module automatically patches Next.js request handling to capture bodies
|
|
5
|
+
* WITHOUT requiring customers to wrap their handlers or change their code.
|
|
6
|
+
*
|
|
7
|
+
* Usage in instrumentation.ts:
|
|
8
|
+
*
|
|
9
|
+
* import { registerSecureNow } from 'securenow/nextjs';
|
|
10
|
+
* import 'securenow/nextjs-auto-capture'; // Just import this line!
|
|
11
|
+
*
|
|
12
|
+
* export function register() {
|
|
13
|
+
* registerSecureNow();
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* That's it! Bodies are now captured automatically.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const { trace } = require('@opentelemetry/api');
|
|
20
|
+
|
|
21
|
+
// Default sensitive fields to redact
|
|
22
|
+
const DEFAULT_SENSITIVE_FIELDS = [
|
|
23
|
+
'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
|
|
24
|
+
'access_token', 'auth', 'credentials', 'mysql_pwd', 'stripeToken',
|
|
25
|
+
'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Redact sensitive fields from an object
|
|
30
|
+
*/
|
|
31
|
+
function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
32
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
33
|
+
|
|
34
|
+
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
35
|
+
|
|
36
|
+
for (const key of Object.keys(redacted)) {
|
|
37
|
+
const lowerKey = key.toLowerCase();
|
|
38
|
+
|
|
39
|
+
if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
|
|
40
|
+
redacted[key] = '[REDACTED]';
|
|
41
|
+
} else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
|
|
42
|
+
redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return redacted;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Safe body capture that doesn't interfere with Next.js
|
|
51
|
+
*/
|
|
52
|
+
async function safeBodyCapture(request, span) {
|
|
53
|
+
if (!span) return;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const contentType = request.headers.get('content-type') || '';
|
|
57
|
+
const maxBodySize = Math.max(1024, parseInt(process.env.SECURENOW_MAX_BODY_SIZE, 10) || 10240);
|
|
58
|
+
const customSensitiveFields = (process.env.SECURENOW_SENSITIVE_FIELDS || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
59
|
+
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
60
|
+
|
|
61
|
+
// Only for supported types
|
|
62
|
+
if (!contentType.includes('application/json') &&
|
|
63
|
+
!contentType.includes('application/graphql') &&
|
|
64
|
+
!contentType.includes('application/x-www-form-urlencoded')) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Try to read from cache if available (Next.js may have already read it)
|
|
69
|
+
let bodyText;
|
|
70
|
+
|
|
71
|
+
// Attempt 1: Check if body was already cached by Next.js
|
|
72
|
+
if (request._bodyText) {
|
|
73
|
+
bodyText = request._bodyText;
|
|
74
|
+
} else {
|
|
75
|
+
// Attempt 2: Try to clone and read
|
|
76
|
+
try {
|
|
77
|
+
const cloned = request.clone();
|
|
78
|
+
bodyText = await cloned.text();
|
|
79
|
+
// Cache it for Next.js
|
|
80
|
+
request._bodyText = bodyText;
|
|
81
|
+
} catch (e) {
|
|
82
|
+
// If clone fails, body was already consumed - skip silently
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (bodyText.length > maxBodySize) {
|
|
88
|
+
span.setAttribute('http.request.body', `[TOO LARGE: ${bodyText.length} bytes]`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Parse and redact
|
|
93
|
+
if (contentType.includes('application/json') || contentType.includes('application/graphql')) {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(bodyText);
|
|
96
|
+
const redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
97
|
+
span.setAttributes({
|
|
98
|
+
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
99
|
+
'http.request.body.type': contentType.includes('graphql') ? 'graphql' : 'json',
|
|
100
|
+
'http.request.body.size': bodyText.length,
|
|
101
|
+
});
|
|
102
|
+
} catch (e) {
|
|
103
|
+
// Parse error - skip
|
|
104
|
+
}
|
|
105
|
+
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
106
|
+
try {
|
|
107
|
+
const params = new URLSearchParams(bodyText);
|
|
108
|
+
const parsed = Object.fromEntries(params);
|
|
109
|
+
const redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
110
|
+
span.setAttributes({
|
|
111
|
+
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
112
|
+
'http.request.body.type': 'form',
|
|
113
|
+
'http.request.body.size': bodyText.length,
|
|
114
|
+
});
|
|
115
|
+
} catch (e) {
|
|
116
|
+
// Parse error - skip
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
// Silently fail - never break the request
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Check if body capture is enabled
|
|
126
|
+
*/
|
|
127
|
+
function isBodyCaptureEnabled() {
|
|
128
|
+
const enabled = String(process.env.SECURENOW_CAPTURE_BODY) === '1' ||
|
|
129
|
+
String(process.env.SECURENOW_CAPTURE_BODY).toLowerCase() === 'true';
|
|
130
|
+
return enabled;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Patch Next.js Request to cache body text
|
|
135
|
+
* This allows us to read the body without consuming it
|
|
136
|
+
*/
|
|
137
|
+
function patchNextRequest() {
|
|
138
|
+
if (typeof Request === 'undefined') return;
|
|
139
|
+
|
|
140
|
+
const originalText = Request.prototype.text;
|
|
141
|
+
const originalJson = Request.prototype.json;
|
|
142
|
+
|
|
143
|
+
// Patch text() to cache result
|
|
144
|
+
Request.prototype.text = async function() {
|
|
145
|
+
if (this._bodyText !== undefined) {
|
|
146
|
+
return this._bodyText;
|
|
147
|
+
}
|
|
148
|
+
const text = await originalText.call(this);
|
|
149
|
+
this._bodyText = text;
|
|
150
|
+
|
|
151
|
+
// Capture for tracing if enabled
|
|
152
|
+
if (isBodyCaptureEnabled() && ['POST', 'PUT', 'PATCH'].includes(this.method)) {
|
|
153
|
+
const span = trace.getActiveSpan();
|
|
154
|
+
if (span) {
|
|
155
|
+
// Schedule capture after this call (non-blocking)
|
|
156
|
+
setImmediate(() => {
|
|
157
|
+
safeBodyCapture(this, span).catch(() => {});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return text;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Patch json() to cache and capture
|
|
166
|
+
Request.prototype.json = async function() {
|
|
167
|
+
// First get text
|
|
168
|
+
const text = await this.text();
|
|
169
|
+
// Then parse
|
|
170
|
+
return JSON.parse(text);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
console.log('[securenow] ✅ Auto-capture: Patched Next.js Request for automatic body capture');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Auto-patch when module is imported
|
|
177
|
+
if (isBodyCaptureEnabled()) {
|
|
178
|
+
try {
|
|
179
|
+
patchNextRequest();
|
|
180
|
+
console.log('[securenow] 📝 Automatic body capture: ENABLED');
|
|
181
|
+
console.log('[securenow] 💡 No code changes needed - bodies captured automatically!');
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.warn('[securenow] ⚠️ Auto-capture patch failed:', error.message);
|
|
184
|
+
console.warn('[securenow] 💡 Body capture disabled. Use manual approach if needed.');
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
console.log('[securenow] 📝 Automatic body capture: DISABLED (set SECURENOW_CAPTURE_BODY=1 to enable)');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = {
|
|
191
|
+
patchNextRequest,
|
|
192
|
+
safeBodyCapture,
|
|
193
|
+
redactSensitiveData,
|
|
194
|
+
isBodyCaptureEnabled,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
|