securenow 5.18.0 → 6.0.1
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/LICENSE +15 -0
- package/README.md +40 -239
- package/cli.js +455 -415
- package/console-instrumentation.js +136 -147
- package/docs/ALL-FRAMEWORKS-QUICKSTART.md +455 -1339
- package/docs/ARCHITECTURE.md +3 -3
- package/docs/AUTO-BODY-CAPTURE.md +1 -1
- package/docs/AUTO-SETUP.md +4 -4
- package/docs/AUTOMATIC-IP-CAPTURE.md +5 -5
- package/docs/BODY-CAPTURE-QUICKSTART.md +2 -2
- package/docs/CHANGELOG-NEXTJS.md +35 -1
- package/docs/CUSTOMER-GUIDE.md +16 -16
- package/docs/EASIEST-SETUP.md +5 -5
- package/docs/ENVIRONMENT-VARIABLES.md +652 -880
- package/docs/EXPRESS-BODY-CAPTURE.md +12 -13
- package/docs/EXPRESS-SETUP-GUIDE.md +720 -719
- package/docs/INDEX.md +4 -22
- package/docs/LOGGING-GUIDE.md +708 -701
- package/docs/LOGGING-QUICKSTART.md +255 -234
- 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-WRAPPER-APPROACH.md +1 -1
- package/docs/QUICKSTART-BODY-CAPTURE.md +2 -2
- package/docs/REDACTION-EXAMPLES.md +1 -1
- package/docs/REQUEST-BODY-CAPTURE.md +10 -19
- 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/nextjs-auto-capture.js +207 -199
- package/nextjs-middleware.js +181 -186
- package/nextjs-webpack-config.js +53 -88
- package/nextjs-wrapper.js +158 -158
- package/nextjs.d.ts +1 -1
- package/nextjs.js +198 -186
- package/package.json +45 -67
- package/postinstall.js +6 -6
- package/register.d.ts +1 -1
- package/register.js +4 -39
- package/tracing.d.ts +1 -2
- package/tracing.js +26 -286
- package/web-vite.mjs +156 -239
- package/CONSUMING-APPS-GUIDE.md +0 -455
- package/NPM_README.md +0 -1933
- package/SKILL-API.md +0 -600
- package/SKILL-CLI.md +0 -409
- package/cidr.js +0 -83
- package/cli/apps.js +0 -585
- package/cli/auth.js +0 -280
- package/cli/client.js +0 -115
- package/cli/config.js +0 -173
- package/cli/firewall.js +0 -100
- package/cli/fp.js +0 -638
- package/cli/init.js +0 -201
- package/cli/monitor.js +0 -440
- package/cli/run.js +0 -133
- package/cli/security.js +0 -1064
- package/cli/ui.js +0 -386
- package/docs/API-KEYS-GUIDE.md +0 -233
- package/docs/AUTO-SETUP-SUMMARY.md +0 -331
- package/docs/BODY-CAPTURE-FIX.md +0 -261
- package/docs/COMPLETION-REPORT.md +0 -408
- package/docs/FINAL-SOLUTION.md +0 -335
- package/docs/FIREWALL-GUIDE.md +0 -426
- package/docs/IMPLEMENTATION-SUMMARY.md +0 -410
- package/docs/NEXTJS-BODY-CAPTURE-COMPARISON.md +0 -323
- package/docs/NEXTJS-SETUP-COMPLETE.md +0 -795
- package/docs/NUXT-GUIDE.md +0 -166
- package/docs/SOLUTION-SUMMARY.md +0 -312
- package/firewall-cloud.js +0 -212
- package/firewall-iptables.js +0 -139
- package/firewall-only.js +0 -38
- package/firewall-tcp.js +0 -74
- package/firewall.js +0 -720
- package/free-trial-banner.js +0 -174
- package/nuxt-server-plugin.mjs +0 -423
- package/nuxt.d.ts +0 -60
- package/nuxt.mjs +0 -75
- package/resolve-ip.js +0 -77
|
@@ -55,24 +55,16 @@ SECURENOW_SENSITIVE_FIELDS=credit_card,email,phone
|
|
|
55
55
|
```
|
|
56
56
|
Parsed into object and sensitive fields redacted.
|
|
57
57
|
|
|
58
|
-
4. **Multipart** (`multipart/form-data`) -
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
```json
|
|
63
|
-
{
|
|
64
|
-
"fields": { "description": "Profile update", "token": "[REDACTED]" },
|
|
65
|
-
"files": [
|
|
66
|
-
{ "field": "avatar", "filename": "photo.jpg", "contentType": "image/jpeg", "size": 524288 }
|
|
67
|
-
]
|
|
68
|
-
}
|
|
58
|
+
4. **Multipart** (`multipart/form-data`) - ❌ NOT Captured
|
|
59
|
+
```
|
|
60
|
+
[MULTIPART - NOT CAPTURED]
|
|
69
61
|
```
|
|
70
|
-
|
|
62
|
+
*File uploads are not captured at all (by design) - too large and unnecessary*
|
|
71
63
|
|
|
72
64
|
### ❌ What's NOT Captured
|
|
73
65
|
|
|
74
66
|
- GET requests (no body)
|
|
75
|
-
- File
|
|
67
|
+
- File uploads (too large)
|
|
76
68
|
- Bodies larger than max size
|
|
77
69
|
- Binary data
|
|
78
70
|
- Non-POST/PUT/PATCH requests
|
|
@@ -226,7 +218,6 @@ mutation Login {
|
|
|
226
218
|
| `SECURENOW_CAPTURE_BODY` | `0` (disabled) | Enable body capture |
|
|
227
219
|
| `SECURENOW_MAX_BODY_SIZE` | `10240` (10KB) | Maximum body size to capture |
|
|
228
220
|
| `SECURENOW_SENSITIVE_FIELDS` | `` | Comma-separated custom sensitive fields |
|
|
229
|
-
| `SECURENOW_CAPTURE_MULTIPART` | `0` (disabled) | Enable multipart/form-data streaming capture (v5.8.0+) |
|
|
230
221
|
|
|
231
222
|
### Programmatic (Next.js)
|
|
232
223
|
|
|
@@ -244,7 +235,7 @@ export function register() {
|
|
|
244
235
|
|
|
245
236
|
---
|
|
246
237
|
|
|
247
|
-
## 🔍 Viewing in
|
|
238
|
+
## 🔍 Viewing in SigNoz
|
|
248
239
|
|
|
249
240
|
### Query Examples
|
|
250
241
|
|
|
@@ -286,7 +277,7 @@ Request bodies may contain personal data. Consider:
|
|
|
286
277
|
|
|
287
278
|
1. **Legal Basis** - Ensure you have legitimate interest or consent
|
|
288
279
|
2. **Data Minimization** - Only capture what you need
|
|
289
|
-
3. **Retention** - Configure
|
|
280
|
+
3. **Retention** - Configure SigNoz retention policies
|
|
290
281
|
4. **Anonymization** - Add more fields to redact list
|
|
291
282
|
|
|
292
283
|
### PCI-DSS Compliance
|
|
@@ -406,7 +397,7 @@ Field matching is case-insensitive and uses `includes()`:
|
|
|
406
397
|
}
|
|
407
398
|
```
|
|
408
399
|
|
|
409
|
-
3. **Filter in
|
|
400
|
+
3. **Filter in SigNoz**
|
|
410
401
|
- Use sampling to reduce volume
|
|
411
402
|
- Set up trace tail sampling
|
|
412
403
|
|
|
@@ -538,7 +529,7 @@ SECURENOW_SENSITIVE_FIELDS="" # Don't do this!
|
|
|
538
529
|
|
|
539
530
|
### Q: Does this work with file uploads?
|
|
540
531
|
|
|
541
|
-
**A:**
|
|
532
|
+
**A:** No, multipart/form-data is not captured (by design). Only metadata is logged.
|
|
542
533
|
|
|
543
534
|
### Q: What's the performance impact?
|
|
544
535
|
|
|
@@ -573,7 +564,7 @@ SECURENOW_SENSITIVE_FIELDS="" # Don't do this!
|
|
|
573
564
|
|
|
574
565
|
3. **Deploy!** Bodies are captured with sensitive data automatically redacted.
|
|
575
566
|
|
|
576
|
-
**View in
|
|
567
|
+
**View in SigNoz:**
|
|
577
568
|
- `http.request.body` - The captured body (redacted)
|
|
578
569
|
- `http.request.body.size` - Body size in bytes
|
|
579
570
|
- `http.request.body.type` - Content type (json, graphql, form)
|
|
@@ -62,7 +62,7 @@ export function register() {
|
|
|
62
62
|
```bash
|
|
63
63
|
# .env.local
|
|
64
64
|
SECURENOW_APPID=my-nextjs-app
|
|
65
|
-
SECURENOW_INSTANCE=http://your-
|
|
65
|
+
SECURENOW_INSTANCE=http://your-signoz:4318
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
### What They Get
|
|
@@ -89,7 +89,7 @@ SECURENOW_INSTANCE=http://your-otlp-backend:4318
|
|
|
89
89
|
- `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`
|
|
90
90
|
3. SecureNow calls `@vercel/otel`'s `registerOTel()`
|
|
91
91
|
4. @vercel/otel handles all the OpenTelemetry setup
|
|
92
|
-
5. Traces flow to
|
|
92
|
+
5. Traces flow to SigNoz
|
|
93
93
|
|
|
94
94
|
### What @vercel/otel Does
|
|
95
95
|
|
|
@@ -185,7 +185,7 @@ All options still work:
|
|
|
185
185
|
```typescript
|
|
186
186
|
registerSecureNow({
|
|
187
187
|
serviceName: 'my-app',
|
|
188
|
-
endpoint: 'http://
|
|
188
|
+
endpoint: 'http://signoz:4318',
|
|
189
189
|
noUuid: false,
|
|
190
190
|
});
|
|
191
191
|
```
|
package/examples/README.md
CHANGED
|
@@ -160,7 +160,7 @@ OTEL_LOG_LEVEL=info
|
|
|
160
160
|
```bash
|
|
161
161
|
# Set in Vercel dashboard:
|
|
162
162
|
SECURENOW_APPID=my-app
|
|
163
|
-
SECURENOW_INSTANCE=http://your-
|
|
163
|
+
SECURENOW_INSTANCE=http://your-signoz:4318
|
|
164
164
|
OTEL_EXPORTER_OTLP_HEADERS="x-api-key=your-key"
|
|
165
165
|
```
|
|
166
166
|
|
|
@@ -180,7 +180,7 @@ npm install securenow@latest
|
|
|
180
180
|
1. Check console for `[securenow] ✅ OpenTelemetry started`
|
|
181
181
|
2. Enable debug mode: `OTEL_LOG_LEVEL=debug`
|
|
182
182
|
3. Run test script: `node examples/test-nextjs-setup.js`
|
|
183
|
-
4. Verify
|
|
183
|
+
4. Verify SigNoz accessibility: `curl http://your-signoz:4318/v1/traces`
|
|
184
184
|
|
|
185
185
|
### Too many spans
|
|
186
186
|
|
|
@@ -206,7 +206,7 @@ registerSecureNow({
|
|
|
206
206
|
```typescript
|
|
207
207
|
registerSecureNow({
|
|
208
208
|
headers: {
|
|
209
|
-
'x-api-key': process.env.
|
|
209
|
+
'x-api-key': process.env.SIGNOZ_API_KEY,
|
|
210
210
|
'x-environment': process.env.NODE_ENV,
|
|
211
211
|
},
|
|
212
212
|
});
|
|
@@ -229,7 +229,7 @@ After setting up:
|
|
|
229
229
|
|
|
230
230
|
1. **Run your app** and verify traces appear
|
|
231
231
|
2. **Test key user flows** to see end-to-end tracing
|
|
232
|
-
3. **Check
|
|
232
|
+
3. **Check SigNoz dashboard** for service map and traces
|
|
233
233
|
4. **Adjust configuration** based on your needs
|
|
234
234
|
5. **Deploy to production** with proper environment variables
|
|
235
235
|
|
|
@@ -242,12 +242,12 @@ $ npm run dev
|
|
|
242
242
|
|
|
243
243
|
[securenow] Next.js integration loading (pid=12345)
|
|
244
244
|
[securenow] 🚀 Next.js App → service.name=my-app-abc123
|
|
245
|
-
[securenow] ✅ OpenTelemetry started for Next.js → http://
|
|
245
|
+
[securenow] ✅ OpenTelemetry started for Next.js → http://signoz:4318/v1/traces
|
|
246
246
|
|
|
247
247
|
✓ Ready in 1.2s
|
|
248
248
|
```
|
|
249
249
|
|
|
250
|
-
Then in
|
|
250
|
+
Then in SigNoz:
|
|
251
251
|
- ✅ See your service in service map
|
|
252
252
|
- ✅ View traces for requests
|
|
253
253
|
- ✅ Analyze performance metrics
|
|
@@ -30,7 +30,7 @@ export function register() {
|
|
|
30
30
|
*
|
|
31
31
|
* Configuration in .env.local:
|
|
32
32
|
* SECURENOW_APPID=my-app
|
|
33
|
-
* SECURENOW_INSTANCE=http://
|
|
33
|
+
* SECURENOW_INSTANCE=http://signoz:4318
|
|
34
34
|
* SECURENOW_CAPTURE_BODY=1
|
|
35
35
|
* SECURENOW_MAX_BODY_SIZE=10240
|
|
36
36
|
* SECURENOW_SENSITIVE_FIELDS=custom_field
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
# Required: Your application identifier
|
|
5
5
|
SECURENOW_APPID=my-nextjs-app
|
|
6
6
|
|
|
7
|
-
# Optional: Your
|
|
7
|
+
# Optional: Your SigNoz/OpenTelemetry collector endpoint
|
|
8
8
|
# Default: https://freetrial.securenow.ai:4318
|
|
9
|
-
SECURENOW_INSTANCE=http://your-
|
|
9
|
+
SECURENOW_INSTANCE=http://your-signoz-server:4318
|
|
10
10
|
|
|
11
11
|
# Optional: API Key or authentication headers
|
|
12
12
|
OTEL_EXPORTER_OTLP_HEADERS="x-api-key=your-api-key-here"
|
|
@@ -21,7 +21,7 @@ export function register() {
|
|
|
21
21
|
* SECURENOW_APPID=my-nextjs-app
|
|
22
22
|
*
|
|
23
23
|
* Optional:
|
|
24
|
-
* SECURENOW_INSTANCE=http://your-
|
|
24
|
+
* SECURENOW_INSTANCE=http://your-signoz-server:4318
|
|
25
25
|
* SECURENOW_NO_UUID=1 # Don't append UUID to service name
|
|
26
26
|
* OTEL_LOG_LEVEL=info # debug|info|warn|error
|
|
27
27
|
* SECURENOW_DISABLE_INSTRUMENTATIONS=fs # Comma-separated list
|
|
@@ -21,7 +21,7 @@ export function register() {
|
|
|
21
21
|
* SECURENOW_APPID=my-nextjs-app
|
|
22
22
|
*
|
|
23
23
|
* Optional:
|
|
24
|
-
* SECURENOW_INSTANCE=http://your-
|
|
24
|
+
* SECURENOW_INSTANCE=http://your-signoz-server:4318
|
|
25
25
|
* SECURENOW_NO_UUID=1 # Don't append UUID to service name
|
|
26
26
|
* OTEL_LOG_LEVEL=info # debug|info|warn|error
|
|
27
27
|
* SECURENOW_DISABLE_INSTRUMENTATIONS=fs # Comma-separated list
|
|
@@ -39,9 +39,9 @@ SECURENOW_LOGGING_ENABLED=1
|
|
|
39
39
|
SECURENOW_APPID=my-nextjs-app
|
|
40
40
|
SECURENOW_INSTANCE=http://localhost:4318
|
|
41
41
|
|
|
42
|
-
# For
|
|
43
|
-
# SECURENOW_INSTANCE=https://ingest.<region>.
|
|
44
|
-
# OTEL_EXPORTER_OTLP_HEADERS="
|
|
42
|
+
# For SigNoz Cloud:
|
|
43
|
+
# SECURENOW_INSTANCE=https://ingest.<region>.signoz.cloud:443
|
|
44
|
+
# OTEL_EXPORTER_OTLP_HEADERS="signoz-ingestion-key=<your-key>"
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
### 4. Enable Instrumentation in `next.config.js`
|
|
@@ -239,9 +239,9 @@ You should see in the console:
|
|
|
239
239
|
SecureNow initialized with logging enabled
|
|
240
240
|
```
|
|
241
241
|
|
|
242
|
-
## View Logs in
|
|
242
|
+
## View Logs in SigNoz
|
|
243
243
|
|
|
244
|
-
1. Open your
|
|
244
|
+
1. Open your SigNoz dashboard
|
|
245
245
|
2. Navigate to **Logs** section
|
|
246
246
|
3. Filter by `service.name = my-nextjs-app`
|
|
247
247
|
4. See all your application logs with:
|
|
@@ -298,4 +298,4 @@ await import('securenow/console-instrumentation'); // Second
|
|
|
298
298
|
|
|
299
299
|
- [Complete Logging Guide](../docs/LOGGING-GUIDE.md)
|
|
300
300
|
- [Next.js Complete Guide](../docs/NEXTJS-GUIDE.md)
|
|
301
|
-
- [
|
|
301
|
+
- [View Logs in SigNoz](https://signoz.io/docs/logs-management/overview/)
|
|
@@ -10,7 +10,7 @@ import { registerSecureNow } from 'securenow/nextjs';
|
|
|
10
10
|
export function register() {
|
|
11
11
|
registerSecureNow({
|
|
12
12
|
serviceName: 'my-nextjs-app',
|
|
13
|
-
endpoint: 'http://your-
|
|
13
|
+
endpoint: 'http://your-signoz-server:4318',
|
|
14
14
|
noUuid: false,
|
|
15
15
|
disableInstrumentations: ['fs', 'dns'],
|
|
16
16
|
headers: {
|
|
@@ -58,7 +58,7 @@ setTimeout(() => {
|
|
|
58
58
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
59
59
|
console.log('✅ All tests passed!');
|
|
60
60
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
61
|
-
console.log('\nCheck your
|
|
61
|
+
console.log('\nCheck your SigNoz dashboard for traces from "test-nextjs-app"\n');
|
|
62
62
|
process.exit(0);
|
|
63
63
|
}, 2000);
|
|
64
64
|
|
package/nextjs-auto-capture.js
CHANGED
|
@@ -1,199 +1,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
|
|
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 =
|
|
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
|
-
|
|
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 in 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 = parseInt(process.env.SECURENOW_MAX_BODY_SIZE || '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
|
+
const originalFormData = Request.prototype.formData;
|
|
143
|
+
|
|
144
|
+
// Patch text() to cache result
|
|
145
|
+
Request.prototype.text = async function() {
|
|
146
|
+
if (this._bodyText !== undefined) {
|
|
147
|
+
return this._bodyText;
|
|
148
|
+
}
|
|
149
|
+
const text = await originalText.call(this);
|
|
150
|
+
this._bodyText = text;
|
|
151
|
+
|
|
152
|
+
// Capture for tracing if enabled
|
|
153
|
+
if (isBodyCaptureEnabled() && ['POST', 'PUT', 'PATCH'].includes(this.method)) {
|
|
154
|
+
const span = trace.getActiveSpan();
|
|
155
|
+
if (span) {
|
|
156
|
+
// Schedule capture after this call (non-blocking)
|
|
157
|
+
setImmediate(() => {
|
|
158
|
+
safeBodyCapture(this, span).catch(() => {});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return text;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Patch json() to cache and capture
|
|
167
|
+
Request.prototype.json = async function() {
|
|
168
|
+
// First get text
|
|
169
|
+
const text = await this.text();
|
|
170
|
+
// Then parse
|
|
171
|
+
return JSON.parse(text);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Patch formData() to cache and capture
|
|
175
|
+
Request.prototype.formData = async function() {
|
|
176
|
+
const text = await this.text();
|
|
177
|
+
const params = new URLSearchParams(text);
|
|
178
|
+
return params;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
console.log('[securenow] ✅ Auto-capture: Patched Next.js Request for automatic body capture');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Auto-patch when module is imported
|
|
185
|
+
if (isBodyCaptureEnabled()) {
|
|
186
|
+
try {
|
|
187
|
+
patchNextRequest();
|
|
188
|
+
console.log('[securenow] 📝 Automatic body capture: ENABLED');
|
|
189
|
+
console.log('[securenow] 💡 No code changes needed - bodies captured automatically!');
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.warn('[securenow] ⚠️ Auto-capture patch failed:', error.message);
|
|
192
|
+
console.warn('[securenow] 💡 Body capture disabled. Use manual approach if needed.');
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
console.log('[securenow] 📝 Automatic body capture: DISABLED (set SECURENOW_CAPTURE_BODY=1 to enable)');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = {
|
|
199
|
+
patchNextRequest,
|
|
200
|
+
safeBodyCapture,
|
|
201
|
+
redactSensitiveData,
|
|
202
|
+
isBodyCaptureEnabled,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
|