securenow 8.6.0 → 8.7.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/cli/security.js +80 -8
- package/cli.js +10 -3
- package/nextjs-auto-capture.js +274 -195
- package/nextjs-middleware.js +268 -185
- package/nextjs-wrapper.js +234 -155
- package/nextjs.js +768 -685
- package/nuxt-server-plugin.mjs +506 -426
- package/package.json +1 -1
- package/tracing.js +844 -758
package/nextjs.js
CHANGED
|
@@ -1,685 +1,768 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* SecureNow Next.js Integration using @vercel/otel
|
|
5
|
-
*
|
|
6
|
-
* Usage in Next.js app:
|
|
7
|
-
*
|
|
8
|
-
* 1. Add securenow to serverExternalPackages and standalone output tracing
|
|
9
|
-
* includes in next.config.js:
|
|
10
|
-
*
|
|
11
|
-
* const nextConfig = {
|
|
12
|
-
* serverExternalPackages: ["securenow"],
|
|
13
|
-
* outputFileTracingIncludes: {
|
|
14
|
-
* "/*": ["<securenow package glob>"],
|
|
15
|
-
* },
|
|
16
|
-
* };
|
|
17
|
-
*
|
|
18
|
-
* 2. Create instrumentation.ts (or .js) in your project root:
|
|
19
|
-
*
|
|
20
|
-
* export async function register() {
|
|
21
|
-
* if (process.env.NEXT_RUNTIME !== "nodejs") return;
|
|
22
|
-
* const securenowNext = await import(/* webpackIgnore: true *\/ "securenow/nextjs");
|
|
23
|
-
* const registerSecureNow = securenowNext.registerSecureNow || securenowNext.default?.registerSecureNow;
|
|
24
|
-
* registerSecureNow({ captureBody: true });
|
|
25
|
-
* await import(/* webpackIgnore: true *\/ "securenow/nextjs-auto-capture");
|
|
26
|
-
* }
|
|
27
|
-
*
|
|
28
|
-
* 3. Run `npx securenow login` and `npx securenow init`.
|
|
29
|
-
* The SDK reads app identity, collector, firewall, capture, and
|
|
30
|
-
* deploymentEnvironment from .securenow/credentials.json.
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
const { randomUUID } = require('crypto');
|
|
34
|
-
const { nodeSdkDefaultTelemetryOptions } = require('./otel-defaults');
|
|
35
|
-
const appConfig = require('./app-config');
|
|
36
|
-
const { resolveClientIpWithDetails } = require('./resolve-ip');
|
|
37
|
-
const otelResources = require('@opentelemetry/resources');
|
|
38
|
-
|
|
39
|
-
const nodeSdkTelemetryOptions = nodeSdkDefaultTelemetryOptions();
|
|
40
|
-
|
|
41
|
-
let isRegistered = false;
|
|
42
|
-
|
|
43
|
-
function requireRuntimeModule(name) {
|
|
44
|
-
const nodeRequire = typeof __non_webpack_require__ === 'function' ? __non_webpack_require__ : eval('require');
|
|
45
|
-
return nodeRequire(name);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function requireNodeBuiltin(name) {
|
|
49
|
-
return requireRuntimeModule(name);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function createResource(attributes) {
|
|
53
|
-
if (typeof otelResources.resourceFromAttributes === 'function') {
|
|
54
|
-
return otelResources.resourceFromAttributes(attributes);
|
|
55
|
-
}
|
|
56
|
-
if (typeof otelResources.Resource === 'function') {
|
|
57
|
-
return new otelResources.Resource(attributes);
|
|
58
|
-
}
|
|
59
|
-
throw new Error('Unsupported @opentelemetry/resources version');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Default sensitive fields to redact from request bodies
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
'http.
|
|
359
|
-
'http.
|
|
360
|
-
'http.
|
|
361
|
-
'http.
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
}
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SecureNow Next.js Integration using @vercel/otel
|
|
5
|
+
*
|
|
6
|
+
* Usage in Next.js app:
|
|
7
|
+
*
|
|
8
|
+
* 1. Add securenow to serverExternalPackages and standalone output tracing
|
|
9
|
+
* includes in next.config.js:
|
|
10
|
+
*
|
|
11
|
+
* const nextConfig = {
|
|
12
|
+
* serverExternalPackages: ["securenow"],
|
|
13
|
+
* outputFileTracingIncludes: {
|
|
14
|
+
* "/*": ["<securenow package glob>"],
|
|
15
|
+
* },
|
|
16
|
+
* };
|
|
17
|
+
*
|
|
18
|
+
* 2. Create instrumentation.ts (or .js) in your project root:
|
|
19
|
+
*
|
|
20
|
+
* export async function register() {
|
|
21
|
+
* if (process.env.NEXT_RUNTIME !== "nodejs") return;
|
|
22
|
+
* const securenowNext = await import(/* webpackIgnore: true *\/ "securenow/nextjs");
|
|
23
|
+
* const registerSecureNow = securenowNext.registerSecureNow || securenowNext.default?.registerSecureNow;
|
|
24
|
+
* registerSecureNow({ captureBody: true });
|
|
25
|
+
* await import(/* webpackIgnore: true *\/ "securenow/nextjs-auto-capture");
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* 3. Run `npx securenow login` and `npx securenow init`.
|
|
29
|
+
* The SDK reads app identity, collector, firewall, capture, and
|
|
30
|
+
* deploymentEnvironment from .securenow/credentials.json.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const { randomUUID } = require('crypto');
|
|
34
|
+
const { nodeSdkDefaultTelemetryOptions } = require('./otel-defaults');
|
|
35
|
+
const appConfig = require('./app-config');
|
|
36
|
+
const { resolveClientIpWithDetails } = require('./resolve-ip');
|
|
37
|
+
const otelResources = require('@opentelemetry/resources');
|
|
38
|
+
|
|
39
|
+
const nodeSdkTelemetryOptions = nodeSdkDefaultTelemetryOptions();
|
|
40
|
+
|
|
41
|
+
let isRegistered = false;
|
|
42
|
+
|
|
43
|
+
function requireRuntimeModule(name) {
|
|
44
|
+
const nodeRequire = typeof __non_webpack_require__ === 'function' ? __non_webpack_require__ : eval('require');
|
|
45
|
+
return nodeRequire(name);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function requireNodeBuiltin(name) {
|
|
49
|
+
return requireRuntimeModule(name);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function createResource(attributes) {
|
|
53
|
+
if (typeof otelResources.resourceFromAttributes === 'function') {
|
|
54
|
+
return otelResources.resourceFromAttributes(attributes);
|
|
55
|
+
}
|
|
56
|
+
if (typeof otelResources.Resource === 'function') {
|
|
57
|
+
return new otelResources.Resource(attributes);
|
|
58
|
+
}
|
|
59
|
+
throw new Error('Unsupported @opentelemetry/resources version');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Default sensitive fields to redact from request bodies.
|
|
63
|
+
// Matched substring-wise against lowercased keys (see redactSensitiveData),
|
|
64
|
+
// so e.g. 'card' also catches 'creditCard' and 'account' also catches
|
|
65
|
+
// 'accountNumber'. Over-redaction here is intentional: a falsely-redacted
|
|
66
|
+
// telemetry value is always safer than a leaked secret. Entries are kept
|
|
67
|
+
// specific enough to avoid nuking broad benign keys (e.g. we use
|
|
68
|
+
// 'firstname'/'lastname'/'fullname', never bare 'name').
|
|
69
|
+
const DEFAULT_SENSITIVE_FIELDS = [
|
|
70
|
+
// credentials / auth
|
|
71
|
+
'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
|
|
72
|
+
'access_token', 'auth', 'authorization', 'bearer', 'credentials',
|
|
73
|
+
'mysql_pwd', 'otp', 'mfa', 'totp', 'sessionid', 'session_id',
|
|
74
|
+
'cookie', 'set-cookie',
|
|
75
|
+
// financial
|
|
76
|
+
'stripeToken', 'card', 'cardnumber', 'ccv', 'cvc', 'cvv',
|
|
77
|
+
'iban', 'account', 'accountnumber', 'routing', 'sortcode', 'taxid',
|
|
78
|
+
// PII
|
|
79
|
+
'ssn', 'pin', 'email', 'e_mail', 'phone', 'mobile', 'dob', 'birthdate',
|
|
80
|
+
'firstname', 'lastname', 'fullname', 'address', 'postcode', 'zip',
|
|
81
|
+
'passport', 'license',
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// Conservative value-shape redactors. Key-name matching misses secrets that
|
|
85
|
+
// land in free-form string values (GraphQL bodies, message fields, etc.), so
|
|
86
|
+
// as a second layer we scrub string VALUES that *look like* a secret/PII.
|
|
87
|
+
// These are intentionally precise/bounded so they don't garble normal prose,
|
|
88
|
+
// and they only ever transform captured telemetry strings (read-only) — never
|
|
89
|
+
// the actual request/response stream. Compiled once at module load.
|
|
90
|
+
const VALUE_REDACTORS = [
|
|
91
|
+
// JWT: three base64url segments. Anchored to the eyJ header so it won't
|
|
92
|
+
// match arbitrary dotted tokens.
|
|
93
|
+
{ name: 'jwt', re: /eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}/g },
|
|
94
|
+
// Bearer/Basic auth header value embedded in a body string.
|
|
95
|
+
{ name: 'bearer', re: /\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi },
|
|
96
|
+
// Stripe-style / SecureNow live+test API keys.
|
|
97
|
+
{ name: 'apikey', re: /\b(?:sk|pk|rk|snk)_(?:live|test)_[A-Za-z0-9]{8,}/g },
|
|
98
|
+
// Email addresses (bounded local/domain parts).
|
|
99
|
+
{ name: 'email', re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,24}\b/g },
|
|
100
|
+
// Credit-card-like: 13–19 digit runs that pass a Luhn check, allowing
|
|
101
|
+
// space/dash grouping. Luhn-gated to avoid clobbering ordinary long numbers.
|
|
102
|
+
{ name: 'card', re: /\b(?:\d[ -]?){13,19}\b/g, luhn: true },
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
// Skip the value-shape scan on very large strings to bound per-body cost.
|
|
106
|
+
const MAX_VALUE_SCAN_LENGTH = 16384;
|
|
107
|
+
|
|
108
|
+
function luhnValid(digits) {
|
|
109
|
+
let sum = 0;
|
|
110
|
+
let alt = false;
|
|
111
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
112
|
+
let d = digits.charCodeAt(i) - 48;
|
|
113
|
+
if (d < 0 || d > 9) return false;
|
|
114
|
+
if (alt) { d *= 2; if (d > 9) d -= 9; }
|
|
115
|
+
sum += d;
|
|
116
|
+
alt = !alt;
|
|
117
|
+
}
|
|
118
|
+
return sum % 10 === 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Redact obvious secret/PII shapes inside a captured string VALUE.
|
|
123
|
+
* Returns the input unchanged when nothing matches (cheap common case).
|
|
124
|
+
*/
|
|
125
|
+
function redactSensitiveValue(value) {
|
|
126
|
+
if (typeof value !== 'string' || value.length === 0) return value;
|
|
127
|
+
if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound the work
|
|
128
|
+
let out = value;
|
|
129
|
+
for (const r of VALUE_REDACTORS) {
|
|
130
|
+
r.re.lastIndex = 0;
|
|
131
|
+
if (r.luhn) {
|
|
132
|
+
out = out.replace(r.re, (m) => {
|
|
133
|
+
const digits = m.replace(/[ -]/g, '');
|
|
134
|
+
if (digits.length < 13 || digits.length > 19) return m;
|
|
135
|
+
return luhnValid(digits) ? '[REDACTED]' : m;
|
|
136
|
+
});
|
|
137
|
+
} else {
|
|
138
|
+
out = out.replace(r.re, '[REDACTED]');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Redact sensitive fields from an object
|
|
146
|
+
*/
|
|
147
|
+
function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
148
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
149
|
+
|
|
150
|
+
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
151
|
+
|
|
152
|
+
for (const key of Object.keys(redacted)) {
|
|
153
|
+
const lowerKey = key.toLowerCase();
|
|
154
|
+
|
|
155
|
+
if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
|
|
156
|
+
redacted[key] = '[REDACTED]';
|
|
157
|
+
} else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
|
|
158
|
+
// Recursively redact nested objects
|
|
159
|
+
redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
|
|
160
|
+
} else if (typeof redacted[key] === 'string') {
|
|
161
|
+
// Second layer: even if the key name looks benign, scrub values that
|
|
162
|
+
// *look like* a secret/PII (JWT, bearer token, API key, email, card).
|
|
163
|
+
redacted[key] = redactSensitiveValue(redacted[key]);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return redacted;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function escapeRegex(str) {
|
|
171
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Redact sensitive data from GraphQL query strings
|
|
176
|
+
*/
|
|
177
|
+
function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
178
|
+
if (!query || typeof query !== 'string') return query;
|
|
179
|
+
|
|
180
|
+
let redacted = query;
|
|
181
|
+
|
|
182
|
+
// Redact sensitive fields in GraphQL arguments and variables
|
|
183
|
+
// Matches patterns like: password: "value" or password:"value" or password:'value'
|
|
184
|
+
sensitiveFields.forEach(field => {
|
|
185
|
+
const escaped = escapeRegex(field);
|
|
186
|
+
const patterns = [
|
|
187
|
+
new RegExp(`(${escaped}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
|
|
188
|
+
new RegExp(`(${escaped}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
patterns.forEach(pattern => {
|
|
192
|
+
redacted = redacted.replace(pattern, (match, prefix, value, suffix) => {
|
|
193
|
+
if (suffix) {
|
|
194
|
+
return `${prefix}[REDACTED]${suffix}`;
|
|
195
|
+
} else {
|
|
196
|
+
return `${prefix}[REDACTED]`;
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Second layer: scrub secret/PII *shapes* anywhere in the (free-form) query
|
|
203
|
+
// body — catches values the key-name pass above can't see.
|
|
204
|
+
redacted = redactSensitiveValue(redacted);
|
|
205
|
+
|
|
206
|
+
return redacted;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Register SecureNow OpenTelemetry for Next.js using @vercel/otel
|
|
211
|
+
* @param {Object} options - Optional configuration
|
|
212
|
+
* @param {string} options.serviceName - Service name (defaults to .securenow/credentials.json app.key/app.name)
|
|
213
|
+
* @param {string} options.endpoint - Advanced OTLP endpoint override (defaults to the SecureNow ingest gateway)
|
|
214
|
+
* @param {string} options.environment - deployment.environment override (defaults to config.runtime.deploymentEnvironment)
|
|
215
|
+
* @param {boolean} options.noUuid - Don't append UUID to service name
|
|
216
|
+
*/
|
|
217
|
+
function registerSecureNow(options = {}) {
|
|
218
|
+
// Only register once
|
|
219
|
+
if (isRegistered) {
|
|
220
|
+
console.log('[securenow] Already registered, skipping...');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Skip for Edge runtime
|
|
225
|
+
if (process.env.NEXT_RUNTIME === 'edge') {
|
|
226
|
+
console.log('[securenow] Skipping Edge runtime (Node.js only)');
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Detect environment outside try block for error handling
|
|
231
|
+
const isVercel = !!(process.env.VERCEL || process.env.VERCEL_ENV || process.env.VERCEL_URL);
|
|
232
|
+
let deploymentEnvironment = appConfig.resolveDeploymentEnvironment();
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
console.log('[securenow] Next.js integration loading (pid=%d)', process.pid);
|
|
236
|
+
|
|
237
|
+
// -------- Configuration --------
|
|
238
|
+
// Resolution order: explicit options -> .securenow/credentials.json -> package.json#name.
|
|
239
|
+
// Telemetry goes to the stable ingest gateway by default; the API gateway
|
|
240
|
+
// routes by app.key to the dashboard-selected instance.
|
|
241
|
+
const resolvedApp = appConfig.resolveAll();
|
|
242
|
+
|
|
243
|
+
const rawBase = (options.serviceName || resolvedApp.appId || '').trim().replace(/^['"]|['"]$/g, '');
|
|
244
|
+
const baseName = rawBase || null;
|
|
245
|
+
// Default: auto-disable suffix when logged in (appId is the routing UUID
|
|
246
|
+
// and the dashboard does exact match). opts.noUuid or config.runtime.noUuid
|
|
247
|
+
// can still override.
|
|
248
|
+
const noUuid = appConfig.resolveNoUuid({ noUuid: options.noUuid });
|
|
249
|
+
deploymentEnvironment = appConfig.normalizeDeploymentEnvironment(
|
|
250
|
+
options.environment || resolvedApp.deploymentEnvironment
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// service.name
|
|
254
|
+
let serviceName;
|
|
255
|
+
if (baseName) {
|
|
256
|
+
serviceName = noUuid ? baseName : `${baseName}-${randomUUID()}`;
|
|
257
|
+
} else {
|
|
258
|
+
serviceName = `nextjs-app-${randomUUID()}`;
|
|
259
|
+
console.warn('[securenow] No app identity resolved. Using fallback: %s', serviceName);
|
|
260
|
+
console.warn('[securenow] Run `npx securenow login` and `npx securenow init` to write .securenow/credentials.json');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// -------- Endpoint Configuration --------
|
|
264
|
+
const resolvedEndpoints = appConfig.resolveEndpoints({ endpoint: options.endpoint || resolvedApp.instance });
|
|
265
|
+
const endpointBase = resolvedEndpoints.endpointBase;
|
|
266
|
+
const tracesUrl = resolvedEndpoints.tracesUrl;
|
|
267
|
+
const logsUrl = resolvedEndpoints.logsUrl;
|
|
268
|
+
const headers = resolvedEndpoints.headers;
|
|
269
|
+
const otelLogLevel = String(appConfig.configValue('otel.logLevel', '') || '').toLowerCase();
|
|
270
|
+
const isDevelopmentRuntime = process.env.NODE_ENV === 'development';
|
|
271
|
+
|
|
272
|
+
console.log('[securenow] Next.js App -> service.name=%s', serviceName);
|
|
273
|
+
console.log('[securenow] Environment: %s', deploymentEnvironment);
|
|
274
|
+
|
|
275
|
+
// -------- Body Capture Configuration --------
|
|
276
|
+
// Opt-out default: set config.capture.body=false (or options.captureBody=false) to disable.
|
|
277
|
+
const captureBody = options.captureBody ?? appConfig.boolConfig('capture.body', true);
|
|
278
|
+
const maxBodySize = appConfig.numberConfig('capture.maxBodySize', 10240, 1024);
|
|
279
|
+
const customSensitiveFields = appConfig.listConfig('capture.sensitiveFields');
|
|
280
|
+
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
281
|
+
|
|
282
|
+
// -------- Log environment detection --------
|
|
283
|
+
if (!isVercel) {
|
|
284
|
+
console.log('[securenow] Self-hosted environment detected (EC2/PM2) - using vanilla SDK');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// -------- Use different initialization based on environment --------
|
|
288
|
+
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
|
|
289
|
+
|
|
290
|
+
// Configure HTTP instrumentation with comprehensive header capture
|
|
291
|
+
const httpInstrumentation = new HttpInstrumentation({
|
|
292
|
+
requireParentforOutgoingSpans: false,
|
|
293
|
+
requireParentforIncomingSpans: false,
|
|
294
|
+
ignoreIncomingRequestHook: (request) => {
|
|
295
|
+
// Never ignore - we want to trace all requests
|
|
296
|
+
return false;
|
|
297
|
+
},
|
|
298
|
+
requestHook: (span, request) => {
|
|
299
|
+
// SYNCHRONOUS ONLY - no async operations to avoid timing issues
|
|
300
|
+
try {
|
|
301
|
+
// Capture all headers
|
|
302
|
+
const headers = request.headers || {};
|
|
303
|
+
|
|
304
|
+
// ======== IP ADDRESS CAPTURE ========
|
|
305
|
+
const ipDetails = resolveClientIpWithDetails(request);
|
|
306
|
+
const forwardedFor = ipDetails.forwardedFor;
|
|
307
|
+
const realIp = ipDetails.realIp;
|
|
308
|
+
const socketIp = ipDetails.socketIp;
|
|
309
|
+
const primaryIp = ipDetails.ip || 'unknown';
|
|
310
|
+
|
|
311
|
+
// ======== PROTOCOL & CONNECTION ========
|
|
312
|
+
const scheme = headers['x-forwarded-proto'] ||
|
|
313
|
+
(request.socket?.encrypted ? 'https' : 'http');
|
|
314
|
+
const host = headers['x-forwarded-host'] || headers['host'] || '';
|
|
315
|
+
const port = headers['x-forwarded-port'] || request.socket?.localPort || '';
|
|
316
|
+
|
|
317
|
+
// ======== REQUEST METADATA ========
|
|
318
|
+
const userAgent = headers['user-agent'] || '';
|
|
319
|
+
const referer = headers['referer'] || headers['referrer'] || '';
|
|
320
|
+
const accept = headers['accept'] || '';
|
|
321
|
+
const acceptLanguage = headers['accept-language'] || '';
|
|
322
|
+
const acceptEncoding = headers['accept-encoding'] || '';
|
|
323
|
+
const contentType = headers['content-type'] || '';
|
|
324
|
+
const contentLength = headers['content-length'] || '';
|
|
325
|
+
const origin = headers['origin'] || '';
|
|
326
|
+
|
|
327
|
+
// ======== PROXY & LOAD BALANCER ========
|
|
328
|
+
const originalUri = headers['x-original-uri'] || '';
|
|
329
|
+
const originalMethod = headers['x-original-method'] || '';
|
|
330
|
+
const requestId = headers['x-request-id'] || headers['x-trace-id'] || headers['x-correlation-id'] || '';
|
|
331
|
+
|
|
332
|
+
// ======== SET ALL ATTRIBUTES ========
|
|
333
|
+
const attributes = {
|
|
334
|
+
// IP & Network
|
|
335
|
+
'http.client_ip': primaryIp,
|
|
336
|
+
'http.client_ip.source': ipDetails.source,
|
|
337
|
+
'http.forwarded_for': forwardedFor || '',
|
|
338
|
+
'http.real_ip': realIp || '',
|
|
339
|
+
'http.socket_ip': socketIp || '',
|
|
340
|
+
'http.proxy.trusted': String(!!ipDetails.trustedProxy),
|
|
341
|
+
'http.request.header.x_forwarded_for': forwardedFor || '',
|
|
342
|
+
'http.request.header.x_real_ip': realIp || '',
|
|
343
|
+
'http.request.header.cf_connecting_ip': ipDetails.cfConnectingIp || '',
|
|
344
|
+
'http.request.header.true_client_ip': ipDetails.trueClientIp || '',
|
|
345
|
+
'http.request.header.x_client_ip': ipDetails.clientIp || '',
|
|
346
|
+
|
|
347
|
+
// Protocol & Host
|
|
348
|
+
'http.scheme': scheme,
|
|
349
|
+
'http.host': host,
|
|
350
|
+
'http.port': port.toString(),
|
|
351
|
+
'http.forwarded_proto': headers['x-forwarded-proto'] || '',
|
|
352
|
+
'http.forwarded_host': headers['x-forwarded-host'] || '',
|
|
353
|
+
|
|
354
|
+
// Request Details
|
|
355
|
+
'http.user_agent': userAgent,
|
|
356
|
+
'http.referer': referer,
|
|
357
|
+
'http.origin': origin,
|
|
358
|
+
'http.accept': accept,
|
|
359
|
+
'http.accept_language': acceptLanguage,
|
|
360
|
+
'http.accept_encoding': acceptEncoding,
|
|
361
|
+
'http.content_type': contentType,
|
|
362
|
+
'http.content_length': contentLength,
|
|
363
|
+
|
|
364
|
+
// Proxy & Routing
|
|
365
|
+
'http.original_uri': originalUri,
|
|
366
|
+
'http.original_method': originalMethod,
|
|
367
|
+
'http.request_id': requestId,
|
|
368
|
+
|
|
369
|
+
// Connection Info
|
|
370
|
+
'http.connection': headers['connection'] || '',
|
|
371
|
+
'http.upgrade': headers['upgrade'] || '',
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// Set all attributes at once
|
|
375
|
+
span.setAttributes(attributes);
|
|
376
|
+
|
|
377
|
+
// ======== GEOGRAPHIC DATA ========
|
|
378
|
+
// Vercel geo headers
|
|
379
|
+
if (headers['x-vercel-ip-country']) {
|
|
380
|
+
span.setAttributes({
|
|
381
|
+
'http.geo.country': headers['x-vercel-ip-country'],
|
|
382
|
+
'http.geo.region': headers['x-vercel-ip-country-region'] || '',
|
|
383
|
+
'http.geo.city': headers['x-vercel-ip-city'] || '',
|
|
384
|
+
'http.geo.latitude': headers['x-vercel-ip-latitude'] || '',
|
|
385
|
+
'http.geo.longitude': headers['x-vercel-ip-longitude'] || '',
|
|
386
|
+
'http.geo.timezone': headers['x-vercel-ip-timezone'] || '',
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Cloudflare geo headers
|
|
391
|
+
if (headers['cf-ipcountry']) {
|
|
392
|
+
span.setAttributes({
|
|
393
|
+
'http.geo.country': headers['cf-ipcountry'],
|
|
394
|
+
'http.geo.cf_ray': headers['cf-ray'] || '',
|
|
395
|
+
'http.geo.cf_visitor': headers['cf-visitor'] || '',
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Cloudflare additional headers
|
|
400
|
+
if (headers['cf-connecting-ip']) {
|
|
401
|
+
span.setAttribute('http.cf.connecting_ip', headers['cf-connecting-ip']);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ======== SECURITY HEADERS ========
|
|
405
|
+
if (headers['x-csrf-token']) {
|
|
406
|
+
span.setAttribute('http.security.csrf_token_present', 'true');
|
|
407
|
+
}
|
|
408
|
+
if (headers['authorization']) {
|
|
409
|
+
span.setAttribute('http.security.auth_present', 'true');
|
|
410
|
+
// Never log the actual token!
|
|
411
|
+
}
|
|
412
|
+
if (headers['cookie']) {
|
|
413
|
+
span.setAttribute('http.security.cookies_present', 'true');
|
|
414
|
+
// Never log actual cookies!
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Debug log in development
|
|
418
|
+
if (isDevelopmentRuntime || otelLogLevel === 'debug') {
|
|
419
|
+
console.log('[securenow] Captured IP: %s (from: %s)',
|
|
420
|
+
primaryIp,
|
|
421
|
+
ipDetails.source || 'unknown'
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// -------- Request Body NOT captured at HTTP instrumentation level --------
|
|
426
|
+
// IMPORTANT: Do NOT attempt to read request.body or listen to 'data' events
|
|
427
|
+
// Next.js manages request streams internally and reading them here causes conflicts
|
|
428
|
+
// Body capture must be done in Next.js middleware using request.clone()
|
|
429
|
+
|
|
430
|
+
} catch (error) {
|
|
431
|
+
// Silently fail to not break the request
|
|
432
|
+
if (otelLogLevel === 'debug') {
|
|
433
|
+
console.error('[securenow] Error in requestHook:', error.message);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
responseHook: (span, response) => {
|
|
438
|
+
try {
|
|
439
|
+
// Capture response metadata
|
|
440
|
+
span.setAttributes({
|
|
441
|
+
'http.status_code': response.statusCode || 0,
|
|
442
|
+
'http.status_message': response.statusMessage || '',
|
|
443
|
+
'http.response.content_type': response.headers?.['content-type'] || '',
|
|
444
|
+
'http.response.content_length': response.headers?.['content-length'] || '',
|
|
445
|
+
});
|
|
446
|
+
} catch (error) {
|
|
447
|
+
// Silently fail
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// -------- Guard against OTLP exporter socket errors --------
|
|
453
|
+
// The OTLP HTTP exporter uses keep-alive connections that can be reset by
|
|
454
|
+
// the remote end (ECONNRESET / "socket hang up"). These transient errors
|
|
455
|
+
// sometimes escape as unhandled exceptions or rejections. We catch them
|
|
456
|
+
// here and log at debug level instead of crashing the host app.
|
|
457
|
+
const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN', 'ENOTFOUND']);
|
|
458
|
+
function _isOtlpTransientError(err) {
|
|
459
|
+
if (!err) return false;
|
|
460
|
+
if (_TRANSIENT_CODES.has(err.code)) return true;
|
|
461
|
+
if (typeof err.message === 'string' && /socket hang up|ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|timed out/i.test(err.message)) return true;
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
function _looksLikeOtlpStack(err) {
|
|
465
|
+
const s = err && err.stack;
|
|
466
|
+
if (!s) return false;
|
|
467
|
+
return /OTLPTraceExporter|OTLPLogExporter|otlp|exporter.*http|BatchSpanProcessor|BatchLogRecordProcessor/i.test(s)
|
|
468
|
+
|| /node:_http_client|ClientRequest|TLSSocket/i.test(s);
|
|
469
|
+
}
|
|
470
|
+
function _looksLikeConfiguredOtlpEndpoint(err) {
|
|
471
|
+
const text = `${err && err.hostname || ''} ${err && err.host || ''} ${err && err.message || ''}`;
|
|
472
|
+
try {
|
|
473
|
+
const hosts = [new URL(tracesUrl).hostname, new URL(logsUrl).hostname].filter(Boolean);
|
|
474
|
+
return hosts.some((host) => host && text.includes(host));
|
|
475
|
+
} catch (_) {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const _diagDebug = otelLogLevel === 'debug';
|
|
480
|
+
let _lastSuppressedOtlpErrorLogAt = 0;
|
|
481
|
+
function _originalConsole(method) {
|
|
482
|
+
const originals = console.__securenow_original || console.__securenowOriginalConsole;
|
|
483
|
+
return (originals && originals[method]) || console[method] || console.log;
|
|
484
|
+
}
|
|
485
|
+
function _formatOtlpError(err) {
|
|
486
|
+
if (!err) return 'unknown error';
|
|
487
|
+
const parts = [err.message || String(err)];
|
|
488
|
+
if (err.code && !parts[0].includes(err.code)) parts.push(`code=${err.code}`);
|
|
489
|
+
if (err.syscall) parts.push(`syscall=${err.syscall}`);
|
|
490
|
+
if (err.hostname) parts.push(`host=${err.hostname}`);
|
|
491
|
+
return parts.join(' ');
|
|
492
|
+
}
|
|
493
|
+
function _reportSuppressedOtlpError(kind, err, origin) {
|
|
494
|
+
if (otelLogLevel === 'none') return;
|
|
495
|
+
const now = Date.now();
|
|
496
|
+
if (!_diagDebug && now - _lastSuppressedOtlpErrorLogAt < 60_000) return;
|
|
497
|
+
_lastSuppressedOtlpErrorLogAt = now;
|
|
498
|
+
const method = _diagDebug ? 'debug' : 'error';
|
|
499
|
+
_originalConsole(method).call(
|
|
500
|
+
console,
|
|
501
|
+
'[securenow] OTLP exporter %s suppressed (%s). Telemetry may be missing until the ingest endpoint is reachable: %s',
|
|
502
|
+
kind,
|
|
503
|
+
origin || 'async',
|
|
504
|
+
_formatOtlpError(err)
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
process.on('uncaughtException', (err, origin) => {
|
|
508
|
+
if (_isOtlpTransientError(err) && (_looksLikeOtlpStack(err) || _looksLikeConfiguredOtlpEndpoint(err))) {
|
|
509
|
+
_reportSuppressedOtlpError('error', err, origin);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
throw err;
|
|
513
|
+
});
|
|
514
|
+
process.on('unhandledRejection', (reason) => {
|
|
515
|
+
if (_isOtlpTransientError(reason) && (_looksLikeOtlpStack(reason) || _looksLikeConfiguredOtlpEndpoint(reason))) {
|
|
516
|
+
_reportSuppressedOtlpError('rejection', reason, 'unhandledRejection');
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
throw reason;
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
if (isVercel) {
|
|
523
|
+
// -------- Vercel Environment: Use @vercel/otel --------
|
|
524
|
+
const { registerOTel } = require('@vercel/otel');
|
|
525
|
+
|
|
526
|
+
registerOTel({
|
|
527
|
+
serviceName: serviceName,
|
|
528
|
+
attributes: {
|
|
529
|
+
'deployment.environment': deploymentEnvironment,
|
|
530
|
+
'service.version': process.env.npm_package_version || process.env.VERCEL_GIT_COMMIT_SHA || undefined,
|
|
531
|
+
'vercel.region': process.env.VERCEL_REGION || undefined,
|
|
532
|
+
},
|
|
533
|
+
instrumentations: [httpInstrumentation],
|
|
534
|
+
instrumentationConfig: {
|
|
535
|
+
fetch: {
|
|
536
|
+
// Propagate context to your backend APIs
|
|
537
|
+
propagateContextUrls: [
|
|
538
|
+
/^https?:\/\/localhost/,
|
|
539
|
+
/^https?:\/\/.*\.vercel\.app/,
|
|
540
|
+
// Add your backend domains here
|
|
541
|
+
],
|
|
542
|
+
// Optionally ignore certain URLs
|
|
543
|
+
ignoreUrls: [
|
|
544
|
+
/_next\/static/,
|
|
545
|
+
/_next\/image/,
|
|
546
|
+
/\.map$/,
|
|
547
|
+
],
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
});
|
|
551
|
+
} else {
|
|
552
|
+
// -------- Self-Hosted (EC2/PM2): Use Vanilla OpenTelemetry SDK --------
|
|
553
|
+
const { NodeSDK } = require('@opentelemetry/sdk-node');
|
|
554
|
+
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
|
|
555
|
+
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
|
|
556
|
+
|
|
557
|
+
const traceExporter = new OTLPTraceExporter({
|
|
558
|
+
url: tracesUrl,
|
|
559
|
+
headers
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
const sdk = new NodeSDK({
|
|
563
|
+
...nodeSdkTelemetryOptions,
|
|
564
|
+
serviceName: serviceName,
|
|
565
|
+
traceExporter: traceExporter,
|
|
566
|
+
instrumentations: [httpInstrumentation],
|
|
567
|
+
resource: createResource({
|
|
568
|
+
[SemanticResourceAttributes.SERVICE_NAME]: serviceName,
|
|
569
|
+
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: deploymentEnvironment,
|
|
570
|
+
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.npm_package_version || undefined,
|
|
571
|
+
}),
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
sdk.start();
|
|
575
|
+
console.log('[securenow] Vanilla SDK initialized for self-hosted environment');
|
|
576
|
+
|
|
577
|
+
// -------- Logging (self-hosted only) --------
|
|
578
|
+
// Opt-out default: set config.logging.enabled=false to disable.
|
|
579
|
+
const loggingEnabled = appConfig.boolConfig('logging.enabled', true);
|
|
580
|
+
if (loggingEnabled) {
|
|
581
|
+
try {
|
|
582
|
+
const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-http');
|
|
583
|
+
const { LoggerProvider, BatchLogRecordProcessor } = require('@opentelemetry/sdk-logs');
|
|
584
|
+
|
|
585
|
+
const logExporter = new OTLPLogExporter({
|
|
586
|
+
url: logsUrl,
|
|
587
|
+
headers,
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
const loggerProvider = new LoggerProvider({
|
|
591
|
+
resource: createResource({
|
|
592
|
+
[SemanticResourceAttributes.SERVICE_NAME]: serviceName,
|
|
593
|
+
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: deploymentEnvironment,
|
|
594
|
+
}),
|
|
595
|
+
processors: [new BatchLogRecordProcessor(logExporter)],
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// Patch console to forward logs as OTLP log records
|
|
599
|
+
const logger = loggerProvider.getLogger('console', '1.0.0');
|
|
600
|
+
const SeverityNumber = { INFO: 9, WARN: 13, ERROR: 17 };
|
|
601
|
+
const origLog = console.log;
|
|
602
|
+
const origWarn = console.warn;
|
|
603
|
+
const origError = console.error;
|
|
604
|
+
|
|
605
|
+
const { context: otelContext, trace: otelTrace } = require('@opentelemetry/api');
|
|
606
|
+
function _emitLog(sn, st, args) {
|
|
607
|
+
try {
|
|
608
|
+
const activeCtx = otelContext.active();
|
|
609
|
+
const spanCtx = otelTrace.getSpanContext(activeCtx);
|
|
610
|
+
logger.emit({
|
|
611
|
+
severityNumber: sn,
|
|
612
|
+
severityText: st,
|
|
613
|
+
body: args.map(String).join(' '),
|
|
614
|
+
...(spanCtx && { context: activeCtx }),
|
|
615
|
+
});
|
|
616
|
+
} catch (_) {}
|
|
617
|
+
}
|
|
618
|
+
console.log = (...args) => {
|
|
619
|
+
origLog.apply(console, args);
|
|
620
|
+
_emitLog(SeverityNumber.INFO, 'INFO', args);
|
|
621
|
+
};
|
|
622
|
+
console.warn = (...args) => {
|
|
623
|
+
origWarn.apply(console, args);
|
|
624
|
+
_emitLog(SeverityNumber.WARN, 'WARN', args);
|
|
625
|
+
};
|
|
626
|
+
console.error = (...args) => {
|
|
627
|
+
origError.apply(console, args);
|
|
628
|
+
_emitLog(SeverityNumber.ERROR, 'ERROR', args);
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
console.log('[securenow] Logging: ENABLED -> %s', logsUrl);
|
|
632
|
+
|
|
633
|
+
// Auto-log every incoming HTTP request/response
|
|
634
|
+
try {
|
|
635
|
+
const http = requireNodeBuiltin('node:http');
|
|
636
|
+
const originalEmit = http.Server.prototype.emit;
|
|
637
|
+
http.Server.prototype.emit = function (event, req, res) {
|
|
638
|
+
if (event === 'request' && req && res) {
|
|
639
|
+
const start = Date.now();
|
|
640
|
+
const method = req.method;
|
|
641
|
+
const url = req.url;
|
|
642
|
+
res.on('finish', () => {
|
|
643
|
+
const reqCtx = otelContext.active();
|
|
644
|
+
const reqSpanCtx = otelTrace.getSpanContext(reqCtx);
|
|
645
|
+
const duration = Date.now() - start;
|
|
646
|
+
const status = res.statusCode;
|
|
647
|
+
const ipDetails = resolveClientIpWithDetails(req);
|
|
648
|
+
const ip = ipDetails.ip || '-';
|
|
649
|
+
const ua = req.headers['user-agent'] || '-';
|
|
650
|
+
const body = `${method} ${url} ${status} ${duration}ms ip=${ip} ua=${ua}`;
|
|
651
|
+
const severity = status >= 500 ? SeverityNumber.ERROR : status >= 400 ? SeverityNumber.WARN : SeverityNumber.INFO;
|
|
652
|
+
const severityText = status >= 500 ? 'ERROR' : status >= 400 ? 'WARN' : 'INFO';
|
|
653
|
+
origLog.call(console, '[securenow] %s %s %d %dms', method, url, status, duration);
|
|
654
|
+
try {
|
|
655
|
+
logger.emit({
|
|
656
|
+
severityNumber: severity,
|
|
657
|
+
severityText,
|
|
658
|
+
body,
|
|
659
|
+
attributes: {
|
|
660
|
+
'http.method': method,
|
|
661
|
+
'http.url': url,
|
|
662
|
+
'http.status_code': status,
|
|
663
|
+
'http.duration_ms': duration,
|
|
664
|
+
'http.client_ip': ip,
|
|
665
|
+
'http.client_ip.source': ipDetails.source || 'unknown',
|
|
666
|
+
'http.socket_ip': ipDetails.socketIp || '',
|
|
667
|
+
'http.forwarded_for': ipDetails.forwardedFor || '',
|
|
668
|
+
'http.real_ip': ipDetails.realIp || '',
|
|
669
|
+
'http.proxy.trusted': String(!!ipDetails.trustedProxy),
|
|
670
|
+
'http.user_agent': ua,
|
|
671
|
+
},
|
|
672
|
+
...(reqSpanCtx && { context: reqCtx }),
|
|
673
|
+
});
|
|
674
|
+
} catch (_) {}
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
return originalEmit.apply(this, arguments);
|
|
678
|
+
};
|
|
679
|
+
console.log('[securenow] HTTP request logging: ENABLED');
|
|
680
|
+
} catch (_) {}
|
|
681
|
+
|
|
682
|
+
// Graceful shutdown for logs
|
|
683
|
+
process.on('SIGTERM', async () => { try { await loggerProvider.shutdown(); } catch (_) {} try { requireRuntimeModule('./firewall').shutdown(); } catch (_) {} });
|
|
684
|
+
process.on('SIGINT', async () => { try { await loggerProvider.shutdown(); } catch (_) {} try { requireRuntimeModule('./firewall').shutdown(); } catch (_) {} });
|
|
685
|
+
} catch (e) {
|
|
686
|
+
console.warn('[securenow] Logging setup failed (missing @opentelemetry/exporter-logs-otlp-http or @opentelemetry/sdk-logs):', e.message);
|
|
687
|
+
}
|
|
688
|
+
} else {
|
|
689
|
+
console.log('[securenow] Logging: DISABLED (config.logging.enabled=false)');
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
isRegistered = true;
|
|
694
|
+
|
|
695
|
+
// Free trial banner (optional - may not be bundled in standalone builds)
|
|
696
|
+
try {
|
|
697
|
+
const { isFreeTrial, patchHttpForBanner } = requireRuntimeModule('./free-trial-banner');
|
|
698
|
+
if (isFreeTrial(endpointBase) && !appConfig.boolConfig('runtime.hideBanner', false)) {
|
|
699
|
+
patchHttpForBanner();
|
|
700
|
+
}
|
|
701
|
+
} catch (_) {}
|
|
702
|
+
|
|
703
|
+
console.log('[securenow] OpenTelemetry started for Next.js -> %s', tracesUrl);
|
|
704
|
+
console.log('[securenow] Auto-capturing comprehensive request metadata:');
|
|
705
|
+
console.log('[securenow] - IP addresses (x-forwarded-for, x-real-ip, socket)');
|
|
706
|
+
console.log('[securenow] - User-Agent, Referer, Origin, Accept headers');
|
|
707
|
+
console.log('[securenow] - Protocol, Host, Port (proxy-aware)');
|
|
708
|
+
console.log('[securenow] - Geographic data (Vercel/Cloudflare)');
|
|
709
|
+
console.log('[securenow] - Request IDs, CSRF tokens, Auth presence');
|
|
710
|
+
console.log('[securenow] - Response status, content-type, content-length');
|
|
711
|
+
console.log('[securenow] Body capture DISABLED at HTTP instrumentation level (prevents Next.js conflicts)');
|
|
712
|
+
if (captureBody) {
|
|
713
|
+
console.log('[securenow] For body capture in Next.js, use: import "securenow/nextjs-auto-capture"');
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Optional test span
|
|
717
|
+
if (appConfig.boolConfig('runtime.testSpan', false)) {
|
|
718
|
+
const api = require('@opentelemetry/api');
|
|
719
|
+
const tracer = api.trace.getTracer('securenow-nextjs');
|
|
720
|
+
const span = tracer.startSpan('securenow.nextjs.startup');
|
|
721
|
+
span.setAttribute('next.runtime', process.env.NEXT_RUNTIME || 'nodejs');
|
|
722
|
+
span.end();
|
|
723
|
+
console.log('[securenow] Test span created');
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
} catch (error) {
|
|
727
|
+
console.error('[securenow] Failed to initialize OpenTelemetry:', error);
|
|
728
|
+
if (isVercel) {
|
|
729
|
+
console.error('[securenow] Make sure you have @vercel/otel installed: npm install @vercel/otel');
|
|
730
|
+
} else {
|
|
731
|
+
console.error('[securenow] Make sure OpenTelemetry dependencies are installed');
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Firewall - runs independently from OTel so it works even if tracing fails.
|
|
736
|
+
// Key and environment come from .securenow/credentials.json (written by
|
|
737
|
+
// login/init or credentials runtime), so no .env entry is needed.
|
|
738
|
+
const firewallOptions = appConfig.resolveFirewallOptions();
|
|
739
|
+
if (firewallOptions.apiKey) {
|
|
740
|
+
try {
|
|
741
|
+
requireRuntimeModule('./firewall').init({
|
|
742
|
+
apiKey: firewallOptions.apiKey,
|
|
743
|
+
appKey: firewallOptions.appKey,
|
|
744
|
+
environment: deploymentEnvironment || firewallOptions.environment,
|
|
745
|
+
apiUrl: firewallOptions.apiUrl,
|
|
746
|
+
apiUrlFallbacks: firewallOptions.apiUrlFallbacks,
|
|
747
|
+
versionCheckInterval: firewallOptions.versionCheckInterval,
|
|
748
|
+
syncInterval: firewallOptions.syncInterval,
|
|
749
|
+
failMode: firewallOptions.failMode,
|
|
750
|
+
statusCode: firewallOptions.statusCode,
|
|
751
|
+
log: firewallOptions.log,
|
|
752
|
+
tcp: firewallOptions.tcp,
|
|
753
|
+
iptables: firewallOptions.iptables,
|
|
754
|
+
cloud: firewallOptions.cloud,
|
|
755
|
+
cloudDryRun: firewallOptions.cloudDryRun,
|
|
756
|
+
cloudflare: firewallOptions.cloudflare,
|
|
757
|
+
aws: firewallOptions.aws,
|
|
758
|
+
gcp: firewallOptions.gcp,
|
|
759
|
+
});
|
|
760
|
+
} catch (e) {
|
|
761
|
+
console.warn('[securenow] Firewall init failed:', e.message);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
module.exports = {
|
|
767
|
+
registerSecureNow,
|
|
768
|
+
};
|