securenow 8.5.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/challenge.js +273 -0
- package/cli/challenges.js +253 -0
- package/cli/security.js +80 -8
- package/cli.js +42 -4
- package/firewall.js +952 -702
- 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 +5 -1
- package/tracing.js +844 -758
package/tracing.js
CHANGED
|
@@ -1,758 +1,844 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Preload with: node --require securenow/register app.js
|
|
5
|
-
*
|
|
6
|
-
* Works for both CJS and ESM apps. On Node >=20.6 the ESM loader hook is
|
|
7
|
-
* auto-registered via module.register() — no --import flag needed.
|
|
8
|
-
* On Node 18 with "type": "module", add the hook manually:
|
|
9
|
-
* node --import @opentelemetry/instrumentation/hook.mjs --require securenow/register app.js
|
|
10
|
-
*
|
|
11
|
-
* Config:
|
|
12
|
-
* Runtime config is read from .securenow/credentials.json.
|
|
13
|
-
* Run `npx securenow login` and `npx securenow init` to create it.
|
|
14
|
-
* Production should mount/copy tokenless runtime credentials to the same path.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const { nodeSdkDefaultTelemetryOptions } = require('./otel-defaults');
|
|
18
|
-
const nodeSdkTelemetryOptions = nodeSdkDefaultTelemetryOptions();
|
|
19
|
-
|
|
20
|
-
const { diag, DiagConsoleLogger, DiagLogLevel, context, trace } = require('@opentelemetry/api');
|
|
21
|
-
const { NodeSDK } = require('@opentelemetry/sdk-node');
|
|
22
|
-
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
|
|
23
|
-
const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-http');
|
|
24
|
-
const { LoggerProvider, BatchLogRecordProcessor } = require('@opentelemetry/sdk-logs');
|
|
25
|
-
const otelResources = require('@opentelemetry/resources');
|
|
26
|
-
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
|
|
27
|
-
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
|
|
28
|
-
const { MongoDBInstrumentation } = require('@opentelemetry/instrumentation-mongodb');
|
|
29
|
-
const { randomUUID } = require('crypto');
|
|
30
|
-
const appConfig = require('./app-config');
|
|
31
|
-
|
|
32
|
-
function createResource(attributes) {
|
|
33
|
-
if (typeof otelResources.resourceFromAttributes === 'function') {
|
|
34
|
-
return otelResources.resourceFromAttributes(attributes);
|
|
35
|
-
}
|
|
36
|
-
if (typeof otelResources.Resource === 'function') {
|
|
37
|
-
return new otelResources.Resource(attributes);
|
|
38
|
-
}
|
|
39
|
-
throw new Error('Unsupported @opentelemetry/resources version');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Default sensitive fields to redact from request bodies
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
if (
|
|
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
|
-
|
|
186
|
-
|
|
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
|
-
if (
|
|
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
|
-
if (
|
|
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
|
-
const
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
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
|
-
const
|
|
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
|
-
if (
|
|
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
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Preload with: node --require securenow/register app.js
|
|
5
|
+
*
|
|
6
|
+
* Works for both CJS and ESM apps. On Node >=20.6 the ESM loader hook is
|
|
7
|
+
* auto-registered via module.register() — no --import flag needed.
|
|
8
|
+
* On Node 18 with "type": "module", add the hook manually:
|
|
9
|
+
* node --import @opentelemetry/instrumentation/hook.mjs --require securenow/register app.js
|
|
10
|
+
*
|
|
11
|
+
* Config:
|
|
12
|
+
* Runtime config is read from .securenow/credentials.json.
|
|
13
|
+
* Run `npx securenow login` and `npx securenow init` to create it.
|
|
14
|
+
* Production should mount/copy tokenless runtime credentials to the same path.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { nodeSdkDefaultTelemetryOptions } = require('./otel-defaults');
|
|
18
|
+
const nodeSdkTelemetryOptions = nodeSdkDefaultTelemetryOptions();
|
|
19
|
+
|
|
20
|
+
const { diag, DiagConsoleLogger, DiagLogLevel, context, trace } = require('@opentelemetry/api');
|
|
21
|
+
const { NodeSDK } = require('@opentelemetry/sdk-node');
|
|
22
|
+
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
|
|
23
|
+
const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-http');
|
|
24
|
+
const { LoggerProvider, BatchLogRecordProcessor } = require('@opentelemetry/sdk-logs');
|
|
25
|
+
const otelResources = require('@opentelemetry/resources');
|
|
26
|
+
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
|
|
27
|
+
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
|
|
28
|
+
const { MongoDBInstrumentation } = require('@opentelemetry/instrumentation-mongodb');
|
|
29
|
+
const { randomUUID } = require('crypto');
|
|
30
|
+
const appConfig = require('./app-config');
|
|
31
|
+
|
|
32
|
+
function createResource(attributes) {
|
|
33
|
+
if (typeof otelResources.resourceFromAttributes === 'function') {
|
|
34
|
+
return otelResources.resourceFromAttributes(attributes);
|
|
35
|
+
}
|
|
36
|
+
if (typeof otelResources.Resource === 'function') {
|
|
37
|
+
return new otelResources.Resource(attributes);
|
|
38
|
+
}
|
|
39
|
+
throw new Error('Unsupported @opentelemetry/resources version');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Default sensitive fields to redact from request bodies.
|
|
43
|
+
// Matched substring-wise against lowercased keys (see redactSensitiveData),
|
|
44
|
+
// so e.g. 'card' also catches 'creditCard' and 'account' also catches
|
|
45
|
+
// 'accountNumber'. Over-redaction here is intentional: a falsely-redacted
|
|
46
|
+
// telemetry value is always safer than a leaked secret. Entries are kept
|
|
47
|
+
// specific enough to avoid nuking broad benign keys (e.g. we use
|
|
48
|
+
// 'firstname'/'lastname'/'fullname', never bare 'name').
|
|
49
|
+
const DEFAULT_SENSITIVE_FIELDS = [
|
|
50
|
+
// credentials / auth
|
|
51
|
+
'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
|
|
52
|
+
'access_token', 'auth', 'authorization', 'bearer', 'credentials',
|
|
53
|
+
'mysql_pwd', 'otp', 'mfa', 'totp', 'sessionid', 'session_id',
|
|
54
|
+
'cookie', 'set-cookie',
|
|
55
|
+
// financial
|
|
56
|
+
'stripeToken', 'card', 'cardnumber', 'ccv', 'cvc', 'cvv',
|
|
57
|
+
'iban', 'account', 'accountnumber', 'routing', 'sortcode', 'taxid',
|
|
58
|
+
// PII
|
|
59
|
+
'ssn', 'pin', 'email', 'e_mail', 'phone', 'mobile', 'dob', 'birthdate',
|
|
60
|
+
'firstname', 'lastname', 'fullname', 'address', 'postcode', 'zip',
|
|
61
|
+
'passport', 'license',
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
// Conservative value-shape redactors. Key-name matching misses secrets that
|
|
65
|
+
// land in free-form string values (GraphQL bodies, message fields, etc.), so
|
|
66
|
+
// as a second layer we scrub string VALUES that *look like* a secret/PII.
|
|
67
|
+
// These are intentionally precise/bounded so they don't garble normal prose,
|
|
68
|
+
// and they only ever transform captured telemetry strings (read-only) — never
|
|
69
|
+
// the actual request/response stream. Compiled once at module load.
|
|
70
|
+
const VALUE_REDACTORS = [
|
|
71
|
+
// JWT: three base64url segments. Anchored to the eyJ header so it won't
|
|
72
|
+
// match arbitrary dotted tokens.
|
|
73
|
+
{ name: 'jwt', re: /eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}/g },
|
|
74
|
+
// Bearer/Basic auth header value embedded in a body string.
|
|
75
|
+
{ name: 'bearer', re: /\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi },
|
|
76
|
+
// Stripe-style / SecureNow live+test API keys.
|
|
77
|
+
{ name: 'apikey', re: /\b(?:sk|pk|rk|snk)_(?:live|test)_[A-Za-z0-9]{8,}/g },
|
|
78
|
+
// Email addresses (bounded local/domain parts).
|
|
79
|
+
{ name: 'email', re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,24}\b/g },
|
|
80
|
+
// Credit-card-like: 13–19 digit runs that pass a Luhn check, allowing
|
|
81
|
+
// space/dash grouping. Luhn-gated to avoid clobbering ordinary long numbers.
|
|
82
|
+
{ name: 'card', re: /\b(?:\d[ -]?){13,19}\b/g, luhn: true },
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
// Skip the value-shape scan on very large strings to bound per-body cost.
|
|
86
|
+
const MAX_VALUE_SCAN_LENGTH = 16384;
|
|
87
|
+
|
|
88
|
+
function luhnValid(digits) {
|
|
89
|
+
let sum = 0;
|
|
90
|
+
let alt = false;
|
|
91
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
92
|
+
let d = digits.charCodeAt(i) - 48;
|
|
93
|
+
if (d < 0 || d > 9) return false;
|
|
94
|
+
if (alt) { d *= 2; if (d > 9) d -= 9; }
|
|
95
|
+
sum += d;
|
|
96
|
+
alt = !alt;
|
|
97
|
+
}
|
|
98
|
+
return sum % 10 === 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Redact obvious secret/PII shapes inside a captured string VALUE.
|
|
103
|
+
* Returns the input unchanged when nothing matches (cheap common case).
|
|
104
|
+
*/
|
|
105
|
+
function redactSensitiveValue(value) {
|
|
106
|
+
if (typeof value !== 'string' || value.length === 0) return value;
|
|
107
|
+
if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound the work
|
|
108
|
+
let out = value;
|
|
109
|
+
for (const r of VALUE_REDACTORS) {
|
|
110
|
+
r.re.lastIndex = 0;
|
|
111
|
+
if (r.luhn) {
|
|
112
|
+
out = out.replace(r.re, (m) => {
|
|
113
|
+
const digits = m.replace(/[ -]/g, '');
|
|
114
|
+
if (digits.length < 13 || digits.length > 19) return m;
|
|
115
|
+
return luhnValid(digits) ? '[REDACTED]' : m;
|
|
116
|
+
});
|
|
117
|
+
} else {
|
|
118
|
+
out = out.replace(r.re, '[REDACTED]');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function escapeRegex(str) {
|
|
125
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Redact sensitive fields from an object
|
|
130
|
+
*/
|
|
131
|
+
function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
132
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
133
|
+
|
|
134
|
+
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
135
|
+
|
|
136
|
+
for (const key of Object.keys(redacted)) {
|
|
137
|
+
const lowerKey = key.toLowerCase();
|
|
138
|
+
|
|
139
|
+
if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
|
|
140
|
+
redacted[key] = '[REDACTED]';
|
|
141
|
+
} else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
|
|
142
|
+
redacted[key] = redactSensitiveData(redacted[key], sensitiveFields);
|
|
143
|
+
} else if (typeof redacted[key] === 'string') {
|
|
144
|
+
// Second layer: even if the key name looks benign, scrub values that
|
|
145
|
+
// *look like* a secret/PII (JWT, bearer token, API key, email, card).
|
|
146
|
+
redacted[key] = redactSensitiveValue(redacted[key]);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return redacted;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Redact sensitive data from GraphQL query strings
|
|
155
|
+
*/
|
|
156
|
+
function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
157
|
+
if (!query || typeof query !== 'string') return query;
|
|
158
|
+
|
|
159
|
+
let redacted = query;
|
|
160
|
+
|
|
161
|
+
// Redact sensitive fields in GraphQL arguments and variables
|
|
162
|
+
sensitiveFields.forEach(field => {
|
|
163
|
+
const escaped = escapeRegex(field);
|
|
164
|
+
const patterns = [
|
|
165
|
+
new RegExp(`(${escaped}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
|
|
166
|
+
new RegExp(`(${escaped}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
patterns.forEach(pattern => {
|
|
170
|
+
redacted = redacted.replace(pattern, (match, prefix, value, suffix) => {
|
|
171
|
+
if (suffix) {
|
|
172
|
+
return `${prefix}[REDACTED]${suffix}`;
|
|
173
|
+
} else {
|
|
174
|
+
return `${prefix}[REDACTED]`;
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Second layer: scrub secret/PII *shapes* anywhere in the (free-form) query
|
|
181
|
+
// body — catches values the key-name pass above can't see.
|
|
182
|
+
redacted = redactSensitiveValue(redacted);
|
|
183
|
+
|
|
184
|
+
return redacted;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// -------- Multipart streaming parser --------
|
|
188
|
+
// Streams through the request without buffering file content.
|
|
189
|
+
// Only part headers and text-field values are kept in memory,
|
|
190
|
+
// so memory stays bounded (~few KB) regardless of upload size.
|
|
191
|
+
|
|
192
|
+
function extractBoundary(contentType) {
|
|
193
|
+
const match = contentType.match(/boundary=(?:"([^"]+)"|([^\s;]+))/i);
|
|
194
|
+
return match ? (match[1] || match[2]) : null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function createMultipartMetaCollector(contentType, sensitiveFields, maxTextFieldSize, onComplete) {
|
|
198
|
+
const boundary = extractBoundary(contentType);
|
|
199
|
+
if (!boundary) { onComplete({ error: 'BOUNDARY_NOT_FOUND' }); return null; }
|
|
200
|
+
|
|
201
|
+
const result = { fields: Object.create(null), files: [] };
|
|
202
|
+
let totalSize = 0;
|
|
203
|
+
let buf = Buffer.alloc(0);
|
|
204
|
+
|
|
205
|
+
const MAX_PARTS = 100;
|
|
206
|
+
let partCount = 0;
|
|
207
|
+
|
|
208
|
+
const FIRST_DELIM = Buffer.from('--' + boundary);
|
|
209
|
+
const DELIM = Buffer.from('\r\n--' + boundary);
|
|
210
|
+
const HDR_END = Buffer.from('\r\n\r\n');
|
|
211
|
+
|
|
212
|
+
let initialized = false;
|
|
213
|
+
let inHeaders = true;
|
|
214
|
+
let isFile = false;
|
|
215
|
+
let fldName = '';
|
|
216
|
+
let fName = '';
|
|
217
|
+
let pCT = '';
|
|
218
|
+
let bodyBytes = 0;
|
|
219
|
+
let textVal = '';
|
|
220
|
+
|
|
221
|
+
function flushPart() {
|
|
222
|
+
if (!fldName || fldName === '__proto__' || fldName === 'constructor' || fldName === 'prototype') return;
|
|
223
|
+
if (isFile) {
|
|
224
|
+
result.files.push({ field: fldName, filename: fName, contentType: pCT || 'unknown', size: bodyBytes });
|
|
225
|
+
} else {
|
|
226
|
+
const lower = fldName.toLowerCase();
|
|
227
|
+
const redact = sensitiveFields.some(f => lower.includes(f.toLowerCase()));
|
|
228
|
+
// Name-match redaction first; otherwise scrub secret/PII *shapes* in the value.
|
|
229
|
+
result.fields[fldName] = redact
|
|
230
|
+
? '[REDACTED]'
|
|
231
|
+
: redactSensitiveValue(textVal.substring(0, maxTextFieldSize));
|
|
232
|
+
}
|
|
233
|
+
fldName = ''; bodyBytes = 0; textVal = ''; partCount++;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function drain() {
|
|
237
|
+
if (!initialized) {
|
|
238
|
+
const i = buf.indexOf(FIRST_DELIM);
|
|
239
|
+
if (i === -1) {
|
|
240
|
+
if (buf.length > FIRST_DELIM.length + 4) buf = buf.slice(buf.length - FIRST_DELIM.length - 4);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
buf = buf.slice(i + FIRST_DELIM.length);
|
|
244
|
+
initialized = true;
|
|
245
|
+
inHeaders = true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let guard = 200;
|
|
249
|
+
while (buf.length > 0 && guard-- > 0 && partCount < MAX_PARTS) {
|
|
250
|
+
if (inHeaders) {
|
|
251
|
+
if (buf.length >= 2 && buf[0] === 0x2D && buf[1] === 0x2D) { buf = Buffer.alloc(0); return; }
|
|
252
|
+
if (buf.length >= 2 && buf[0] === 0x0D && buf[1] === 0x0A) { buf = buf.slice(2); continue; }
|
|
253
|
+
|
|
254
|
+
const hi = buf.indexOf(HDR_END);
|
|
255
|
+
if (hi === -1) return;
|
|
256
|
+
|
|
257
|
+
const hdr = buf.slice(0, hi).toString('latin1');
|
|
258
|
+
buf = buf.slice(hi + 4);
|
|
259
|
+
|
|
260
|
+
const nm = hdr.match(/name="([^"]+)"/);
|
|
261
|
+
const fn = hdr.match(/filename="([^"]*)"/);
|
|
262
|
+
const ct = hdr.match(/Content-Type:\s*(.+)/i);
|
|
263
|
+
fldName = nm ? nm[1] : '';
|
|
264
|
+
fName = fn ? fn[1] : '';
|
|
265
|
+
pCT = ct ? ct[1].trim() : '';
|
|
266
|
+
isFile = !!fn;
|
|
267
|
+
bodyBytes = 0;
|
|
268
|
+
textVal = '';
|
|
269
|
+
inHeaders = false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const di = buf.indexOf(DELIM);
|
|
273
|
+
if (di === -1) {
|
|
274
|
+
const safe = Math.max(0, buf.length - DELIM.length - 2);
|
|
275
|
+
if (safe > 0) {
|
|
276
|
+
bodyBytes += safe;
|
|
277
|
+
if (!isFile && textVal.length < maxTextFieldSize) {
|
|
278
|
+
textVal += buf.slice(0, safe).toString('utf8').substring(0, maxTextFieldSize - textVal.length);
|
|
279
|
+
}
|
|
280
|
+
buf = buf.slice(safe);
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
bodyBytes += di;
|
|
286
|
+
if (!isFile && textVal.length < maxTextFieldSize) {
|
|
287
|
+
textVal += buf.slice(0, di).toString('utf8').substring(0, maxTextFieldSize - textVal.length);
|
|
288
|
+
}
|
|
289
|
+
flushPart();
|
|
290
|
+
buf = buf.slice(di + DELIM.length);
|
|
291
|
+
inHeaders = true;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function onData(chunk) {
|
|
296
|
+
const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
297
|
+
totalSize += data.length;
|
|
298
|
+
buf = Buffer.concat([buf, data]);
|
|
299
|
+
drain();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function onEnd() {
|
|
303
|
+
try {
|
|
304
|
+
if (!inHeaders && fldName) {
|
|
305
|
+
bodyBytes += buf.length;
|
|
306
|
+
if (!isFile) textVal += buf.toString('utf8').substring(0, maxTextFieldSize - textVal.length);
|
|
307
|
+
flushPart();
|
|
308
|
+
}
|
|
309
|
+
onComplete({ parsed: result, totalSize });
|
|
310
|
+
} catch (e) {
|
|
311
|
+
onComplete({ error: 'PARSE_ERROR' });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return { onData, onEnd };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFieldSize, onComplete) {
|
|
319
|
+
const collector = createMultipartMetaCollector(contentType, sensitiveFields, maxTextFieldSize, onComplete);
|
|
320
|
+
if (!collector) return;
|
|
321
|
+
request.on('data', collector.onData);
|
|
322
|
+
request.on('end', collector.onEnd);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// -------- ESM detection --------
|
|
326
|
+
// register.js auto-registers the hook via module.register() on Node >=20.6.
|
|
327
|
+
// This warning only fires if BOTH --import AND module.register() were skipped
|
|
328
|
+
// (e.g. Node 18, or require('securenow/tracing') called directly without register.js).
|
|
329
|
+
(() => {
|
|
330
|
+
try {
|
|
331
|
+
const fs = require('fs');
|
|
332
|
+
const path = require('path');
|
|
333
|
+
const pkgPath = path.resolve(process.cwd(), 'package.json');
|
|
334
|
+
if (fs.existsSync(pkgPath)) {
|
|
335
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
336
|
+
if (pkg.type === 'module') {
|
|
337
|
+
const execArgv = process.execArgv.join(' ');
|
|
338
|
+
const hasCliHook = execArgv.includes('hook.mjs') || execArgv.includes('import-in-the-middle');
|
|
339
|
+
const hasModuleRegister = typeof require('node:module').register === 'function';
|
|
340
|
+
if (!hasCliHook && !hasModuleRegister) {
|
|
341
|
+
console.warn('[securenow] âš ï¸ ESM app detected ("type": "module") but no ESM loader hook available.');
|
|
342
|
+
console.warn('[securenow] Upgrade to Node >=20.6 (recommended) or add: --import @opentelemetry/instrumentation/hook.mjs');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
} catch (_) {}
|
|
347
|
+
})();
|
|
348
|
+
|
|
349
|
+
// -------- diagnostics --------
|
|
350
|
+
const diagLevel = String(appConfig.configValue('otel.logLevel', '') || '').toLowerCase();
|
|
351
|
+
(() => {
|
|
352
|
+
const level = diagLevel === 'debug' ? DiagLogLevel.DEBUG :
|
|
353
|
+
diagLevel === 'info' ? DiagLogLevel.INFO :
|
|
354
|
+
diagLevel === 'warn' ? DiagLogLevel.WARN :
|
|
355
|
+
diagLevel === 'error' ? DiagLogLevel.ERROR : DiagLogLevel.NONE;
|
|
356
|
+
diag.setLogger(new DiagConsoleLogger(), level);
|
|
357
|
+
console.log('[securenow] preload loaded pid=%d', process.pid);
|
|
358
|
+
})();
|
|
359
|
+
|
|
360
|
+
// -------- endpoints & app resolution --------
|
|
361
|
+
// Resolution order for endpoint/appId/apiKey: .securenow/credentials.json -> package.json#name -> defaults.
|
|
362
|
+
const resolvedApp = appConfig.resolveAll();
|
|
363
|
+
const resolvedEndpoints = appConfig.resolveEndpoints();
|
|
364
|
+
|
|
365
|
+
const endpointBase = resolvedEndpoints.endpointBase;
|
|
366
|
+
const tracesUrl = resolvedEndpoints.tracesUrl;
|
|
367
|
+
const logsUrl = resolvedEndpoints.logsUrl;
|
|
368
|
+
|
|
369
|
+
// resolveEndpoints() also adds app routing and runtime auth headers when
|
|
370
|
+
// explicit OTLP headers did not provide them.
|
|
371
|
+
const headers = resolvedEndpoints.headers;
|
|
372
|
+
|
|
373
|
+
// -------- naming rules --------
|
|
374
|
+
const rawBase = (resolvedApp.appId || '').trim().replace(/^['"]|['"]$/g, '');
|
|
375
|
+
const baseName = rawBase || null;
|
|
376
|
+
// Auto-disables the per-worker suffix when we resolved a routing UUID from
|
|
377
|
+
// credentials — the dashboard does exact-match IN on service.name, so any
|
|
378
|
+
// suffix breaks routing. config.runtime.noUuid can override.
|
|
379
|
+
const noUuid = appConfig.resolveNoUuid();
|
|
380
|
+
const strict = appConfig.boolConfig('runtime.strict', false);
|
|
381
|
+
const inPm2Cluster = !!(process.env.NODE_APP_INSTANCE || process.env.pm_id);
|
|
382
|
+
|
|
383
|
+
// Fail fast in cluster if base is missing (no more "free" names)
|
|
384
|
+
if (!baseName && inPm2Cluster && strict) {
|
|
385
|
+
console.error('[securenow] FATAL: app identity missing in cluster (pid=%d). Exiting due to config.runtime.strict=true.', process.pid);
|
|
386
|
+
// small delay so the log flushes
|
|
387
|
+
setTimeout(() => process.exit(1), 10);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// service.name
|
|
391
|
+
let serviceName;
|
|
392
|
+
if (baseName) {
|
|
393
|
+
serviceName = noUuid ? baseName : `${baseName}-${randomUUID()}`;
|
|
394
|
+
} else {
|
|
395
|
+
// last-resort fallback (only if STRlCT is off). You can rename this to make it obvious in monitoring.
|
|
396
|
+
serviceName = `securenow-free-${randomUUID()}`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// service.instance.id = <appid-or-fallback>-<uuid> (unique per worker)
|
|
400
|
+
const instancePrefix = baseName || 'securenow';
|
|
401
|
+
const serviceInstanceId = `${instancePrefix}-${randomUUID()}`;
|
|
402
|
+
|
|
403
|
+
// Loud line per worker to prove what was used
|
|
404
|
+
console.log('[securenow] pid=%d appId=%s instance=%s apiKey=%s → service.name=%s instance.id=%s',
|
|
405
|
+
process.pid,
|
|
406
|
+
JSON.stringify(baseName),
|
|
407
|
+
JSON.stringify(endpointBase),
|
|
408
|
+
resolvedApp.appKey ? 'set' : 'none',
|
|
409
|
+
serviceName,
|
|
410
|
+
serviceInstanceId
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
// -------- instrumentations --------
|
|
414
|
+
const disabledMap = {};
|
|
415
|
+
for (const n of appConfig.listConfig('otel.disableInstrumentations')) {
|
|
416
|
+
disabledMap[n] = { enabled: false };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// -------- Body Capture Configuration --------
|
|
420
|
+
// Opt-out defaults: set config.capture.body=false to disable.
|
|
421
|
+
const captureBody = appConfig.boolConfig('capture.body', true);
|
|
422
|
+
const maxBodySize = appConfig.numberConfig('capture.maxBodySize', 10240, 1024);
|
|
423
|
+
const customSensitiveFields = appConfig.listConfig('capture.sensitiveFields');
|
|
424
|
+
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
425
|
+
|
|
426
|
+
const captureMultipart = appConfig.boolConfig('capture.multipart', true);
|
|
427
|
+
|
|
428
|
+
const BODY_CAPTURE_PATCH = Symbol.for('securenow.bodyCapture.emitPatch');
|
|
429
|
+
|
|
430
|
+
function installRequestBodyObserver(span, request, contentType) {
|
|
431
|
+
if (!request || request[BODY_CAPTURE_PATCH]) return;
|
|
432
|
+
|
|
433
|
+
const normalizedContentType = String(contentType || '').toLowerCase();
|
|
434
|
+
const isStructuredBody = captureBody && (
|
|
435
|
+
normalizedContentType.includes('application/json') ||
|
|
436
|
+
normalizedContentType.includes('application/graphql') ||
|
|
437
|
+
normalizedContentType.includes('application/x-www-form-urlencoded')
|
|
438
|
+
);
|
|
439
|
+
const isMultipartBody = normalizedContentType.includes('multipart/form-data');
|
|
440
|
+
|
|
441
|
+
if (!isStructuredBody && !isMultipartBody) return;
|
|
442
|
+
|
|
443
|
+
if (isMultipartBody && !captureMultipart) {
|
|
444
|
+
span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
|
|
445
|
+
span.setAttribute('http.request.body.type', 'multipart');
|
|
446
|
+
span.setAttribute('http.request.body.note', 'Multipart capture disabled by config.capture.multipart=false');
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
let chunks = [];
|
|
451
|
+
let size = 0;
|
|
452
|
+
let structuredDone = false;
|
|
453
|
+
|
|
454
|
+
const multipartCollector = isMultipartBody && captureMultipart
|
|
455
|
+
? createMultipartMetaCollector(normalizedContentType, allSensitiveFields, 1000, ({ error, parsed, totalSize }) => {
|
|
456
|
+
try {
|
|
457
|
+
if (error === 'BOUNDARY_NOT_FOUND') {
|
|
458
|
+
span.setAttribute('http.request.body', '[MULTIPART - BOUNDARY NOT FOUND]');
|
|
459
|
+
span.setAttribute('http.request.body.type', 'multipart');
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (error) {
|
|
463
|
+
span.setAttribute('http.request.body', '[MULTIPART - PARSE ERROR]');
|
|
464
|
+
span.setAttribute('http.request.body.type', 'multipart');
|
|
465
|
+
span.setAttribute('http.request.body.parse_error', true);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
span.setAttributes({
|
|
469
|
+
'http.request.body': JSON.stringify(parsed).substring(0, maxBodySize),
|
|
470
|
+
'http.request.body.type': 'multipart',
|
|
471
|
+
'http.request.body.size': totalSize,
|
|
472
|
+
'http.request.body.fields_count': Object.keys(parsed.fields).length,
|
|
473
|
+
'http.request.body.files_count': parsed.files.length,
|
|
474
|
+
});
|
|
475
|
+
} catch (e) {
|
|
476
|
+
span.setAttribute('http.request.body', '[MULTIPART - PARSE ERROR]');
|
|
477
|
+
span.setAttribute('http.request.body.type', 'multipart');
|
|
478
|
+
span.setAttribute('http.request.body.parse_error', true);
|
|
479
|
+
}
|
|
480
|
+
})
|
|
481
|
+
: null;
|
|
482
|
+
|
|
483
|
+
if (isMultipartBody && captureMultipart && !multipartCollector) return;
|
|
484
|
+
|
|
485
|
+
function finishStructuredCapture() {
|
|
486
|
+
if (!isStructuredBody || structuredDone) return;
|
|
487
|
+
structuredDone = true;
|
|
488
|
+
|
|
489
|
+
if (size > maxBodySize) {
|
|
490
|
+
span.setAttribute('http.request.body', `[TOO LARGE: ${size} bytes]`);
|
|
491
|
+
span.setAttribute('http.request.body.size', size);
|
|
492
|
+
chunks = [];
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (chunks.length === 0) return;
|
|
497
|
+
|
|
498
|
+
const body = Buffer.concat(chunks).toString('utf8');
|
|
499
|
+
chunks = [];
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
if (normalizedContentType.includes('application/graphql')) {
|
|
503
|
+
const redacted = redactGraphQLQuery(body, allSensitiveFields);
|
|
504
|
+
span.setAttributes({
|
|
505
|
+
'http.request.body': redacted.substring(0, maxBodySize),
|
|
506
|
+
'http.request.body.type': 'graphql',
|
|
507
|
+
'http.request.body.size': size,
|
|
508
|
+
});
|
|
509
|
+
} else if (normalizedContentType.includes('application/json')) {
|
|
510
|
+
const parsed = JSON.parse(body);
|
|
511
|
+
const redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
512
|
+
span.setAttributes({
|
|
513
|
+
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
514
|
+
'http.request.body.type': 'json',
|
|
515
|
+
'http.request.body.size': size,
|
|
516
|
+
});
|
|
517
|
+
} else if (normalizedContentType.includes('application/x-www-form-urlencoded')) {
|
|
518
|
+
const parsed = Object.fromEntries(new URLSearchParams(body));
|
|
519
|
+
const redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
520
|
+
span.setAttributes({
|
|
521
|
+
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
522
|
+
'http.request.body.type': 'form',
|
|
523
|
+
'http.request.body.size': size,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
} catch (e) {
|
|
527
|
+
span.setAttribute('http.request.body', '[UNPARSEABLE - REDACTED FOR SAFETY]');
|
|
528
|
+
span.setAttribute('http.request.body.parse_error', true);
|
|
529
|
+
span.setAttribute('http.request.body.size', size);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const originalEmit = request.emit;
|
|
534
|
+
request[BODY_CAPTURE_PATCH] = true;
|
|
535
|
+
request.emit = function securenowObservedEmit(event, ...args) {
|
|
536
|
+
try {
|
|
537
|
+
if (event === 'data' && args.length > 0) {
|
|
538
|
+
const chunk = Buffer.isBuffer(args[0]) ? args[0] : Buffer.from(args[0]);
|
|
539
|
+
if (isStructuredBody) {
|
|
540
|
+
size += chunk.length;
|
|
541
|
+
if (size <= maxBodySize) chunks.push(chunk);
|
|
542
|
+
}
|
|
543
|
+
if (multipartCollector) multipartCollector.onData(chunk);
|
|
544
|
+
} else if (event === 'end') {
|
|
545
|
+
finishStructuredCapture();
|
|
546
|
+
if (multipartCollector) multipartCollector.onEnd();
|
|
547
|
+
}
|
|
548
|
+
} catch (_) {
|
|
549
|
+
// Body capture must never interfere with the application request stream.
|
|
550
|
+
}
|
|
551
|
+
return originalEmit.apply(this, [event, ...args]);
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// -------- Trusted proxy IP resolution --------
|
|
556
|
+
const { resolveClientIpWithDetails } = require('./resolve-ip');
|
|
557
|
+
|
|
558
|
+
// Configure HTTP instrumentation with body capture
|
|
559
|
+
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
|
|
560
|
+
const httpInstrumentation = new HttpInstrumentation({
|
|
561
|
+
requestHook: (span, request) => {
|
|
562
|
+
try {
|
|
563
|
+
const ipDetails = resolveClientIpWithDetails(request);
|
|
564
|
+
if (ipDetails.ip) {
|
|
565
|
+
span.setAttribute('http.client_ip', ipDetails.ip);
|
|
566
|
+
span.setAttribute('http.client_ip.source', ipDetails.source);
|
|
567
|
+
span.setAttribute('http.socket_ip', ipDetails.socketIp || '');
|
|
568
|
+
span.setAttribute('http.proxy.trusted', String(!!ipDetails.trustedProxy));
|
|
569
|
+
if (ipDetails.forwardedFor) {
|
|
570
|
+
span.setAttribute('http.forwarded_for', ipDetails.forwardedFor);
|
|
571
|
+
span.setAttribute('http.request.header.x_forwarded_for', ipDetails.forwardedFor);
|
|
572
|
+
}
|
|
573
|
+
if (ipDetails.realIp) {
|
|
574
|
+
span.setAttribute('http.real_ip', ipDetails.realIp);
|
|
575
|
+
span.setAttribute('http.request.header.x_real_ip', ipDetails.realIp);
|
|
576
|
+
}
|
|
577
|
+
if (ipDetails.cfConnectingIp) {
|
|
578
|
+
span.setAttribute('http.cf.connecting_ip', ipDetails.cfConnectingIp);
|
|
579
|
+
span.setAttribute('http.request.header.cf_connecting_ip', ipDetails.cfConnectingIp);
|
|
580
|
+
}
|
|
581
|
+
if (ipDetails.trueClientIp) {
|
|
582
|
+
span.setAttribute('http.request.header.true_client_ip', ipDetails.trueClientIp);
|
|
583
|
+
}
|
|
584
|
+
if (ipDetails.clientIp) {
|
|
585
|
+
span.setAttribute('http.request.header.x_client_ip', ipDetails.clientIp);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (request.headers) {
|
|
590
|
+
const SKIP_HEADERS = new Set(['cookie', 'authorization', 'proxy-authorization', 'set-cookie', 'x-api-key', 'x-auth-token']);
|
|
591
|
+
const safe = {};
|
|
592
|
+
for (const [k, v] of Object.entries(request.headers)) {
|
|
593
|
+
if (SKIP_HEADERS.has(k.toLowerCase())) { safe[k] = '[REDACTED]'; continue; }
|
|
594
|
+
safe[k] = typeof v === 'string' ? v.substring(0, 500) : String(v);
|
|
595
|
+
}
|
|
596
|
+
const serialized = JSON.stringify(safe);
|
|
597
|
+
if (serialized.length <= 8192) {
|
|
598
|
+
span.setAttribute('http.request.headers', serialized);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if ((captureBody || captureMultipart) && request.method && ['POST', 'PUT', 'PATCH'].includes(request.method)) {
|
|
603
|
+
const contentType = request.headers['content-type'] || '';
|
|
604
|
+
installRequestBodyObserver(span, request, contentType);
|
|
605
|
+
}
|
|
606
|
+
} catch (error) {
|
|
607
|
+
// Silently fail
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
// -------- Logging Configuration --------
|
|
613
|
+
// Opt-out default: set config.logging.enabled=false to disable.
|
|
614
|
+
const loggingEnabled = appConfig.boolConfig('logging.enabled', true);
|
|
615
|
+
|
|
616
|
+
// Create shared resource for both traces and logs
|
|
617
|
+
const sharedResource = createResource({
|
|
618
|
+
[SemanticResourceAttributes.SERVICE_NAME]: serviceName,
|
|
619
|
+
[SemanticResourceAttributes.SERVICE_INSTANCE_ID]: serviceInstanceId,
|
|
620
|
+
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: appConfig.resolveDeploymentEnvironment(),
|
|
621
|
+
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.npm_package_version || undefined,
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// Initialize LoggerProvider if logging is enabled
|
|
625
|
+
let loggerProvider = null;
|
|
626
|
+
|
|
627
|
+
if (loggingEnabled) {
|
|
628
|
+
const logExporter = new OTLPLogExporter({
|
|
629
|
+
url: logsUrl,
|
|
630
|
+
headers
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
const batchLogProcessor = new BatchLogRecordProcessor(logExporter);
|
|
634
|
+
loggerProvider = new LoggerProvider({
|
|
635
|
+
resource: sharedResource,
|
|
636
|
+
processors: [batchLogProcessor],
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
// Auto-patch console.* so every log/warn/error becomes an OTel log record
|
|
640
|
+
const _logger = loggerProvider.getLogger('console', '1.0.0');
|
|
641
|
+
const _orig = console.__securenow_original || { log: console.log, info: console.info, warn: console.warn, error: console.error, debug: console.debug };
|
|
642
|
+
if (!console.__securenow_original) console.__securenow_original = _orig;
|
|
643
|
+
const SEV = { DEBUG: 5, INFO: 9, WARN: 13, ERROR: 17 };
|
|
644
|
+
function _emit(sn, st, args) {
|
|
645
|
+
try {
|
|
646
|
+
const activeCtx = context.active();
|
|
647
|
+
const spanCtx = trace.getSpanContext(activeCtx);
|
|
648
|
+
_logger.emit({
|
|
649
|
+
severityNumber: sn,
|
|
650
|
+
severityText: st,
|
|
651
|
+
body: args.map(a => (typeof a === 'object' && a !== null) ? JSON.stringify(a) : String(a)).join(' '),
|
|
652
|
+
attributes: { 'log.source': 'console', 'log.method': st.toLowerCase() },
|
|
653
|
+
...(spanCtx && { context: activeCtx }),
|
|
654
|
+
});
|
|
655
|
+
} catch (_) {}
|
|
656
|
+
}
|
|
657
|
+
console.log = function (...a) { _emit(SEV.INFO, 'INFO', a); _orig.log.apply(console, a); };
|
|
658
|
+
console.info = function (...a) { _emit(SEV.INFO, 'INFO', a); _orig.info.apply(console, a); };
|
|
659
|
+
console.warn = function (...a) { _emit(SEV.WARN, 'WARN', a); _orig.warn.apply(console, a); };
|
|
660
|
+
console.error = function (...a) { _emit(SEV.ERROR, 'ERROR', a); _orig.error.apply(console, a); };
|
|
661
|
+
console.debug = function (...a) { _emit(SEV.DEBUG, 'DEBUG', a); _orig.debug.apply(console, a); };
|
|
662
|
+
console.__securenow_patched = true;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// -------- Guard against OTLP exporter socket errors --------
|
|
666
|
+
// The OTLP HTTP exporter uses keep-alive connections that can be reset by the
|
|
667
|
+
// remote end (ECONNRESET / "socket hang up"). These transient errors sometimes
|
|
668
|
+
// escape as unhandled exceptions or rejections because the underlying HTTP
|
|
669
|
+
// request's error path isn't fully covered by the OTel library. We install
|
|
670
|
+
// targeted process-level handlers to catch them and log at debug level instead
|
|
671
|
+
// of crashing the host app.
|
|
672
|
+
const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN', 'ENOTFOUND']);
|
|
673
|
+
function _isOtlpTransientError(err) {
|
|
674
|
+
if (!err) return false;
|
|
675
|
+
if (_TRANSIENT_CODES.has(err.code)) return true;
|
|
676
|
+
if (typeof err.message === 'string' && /socket hang up|ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|timed out/i.test(err.message)) return true;
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
function _looksLikeOtlpStack(err) {
|
|
680
|
+
const s = err && err.stack;
|
|
681
|
+
if (!s) return false;
|
|
682
|
+
return /OTLPTraceExporter|OTLPLogExporter|otlp|exporter.*http|BatchSpanProcessor|BatchLogRecordProcessor/i.test(s)
|
|
683
|
+
|| /node:_http_client|ClientRequest|TLSSocket/i.test(s);
|
|
684
|
+
}
|
|
685
|
+
function _looksLikeConfiguredOtlpEndpoint(err) {
|
|
686
|
+
const text = `${err && err.hostname || ''} ${err && err.host || ''} ${err && err.message || ''}`;
|
|
687
|
+
try {
|
|
688
|
+
const hosts = [new URL(tracesUrl).hostname, new URL(logsUrl).hostname].filter(Boolean);
|
|
689
|
+
return hosts.some((host) => host && text.includes(host));
|
|
690
|
+
} catch (_) {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
function _originalConsole(method) {
|
|
695
|
+
const originals = console.__securenow_original || console.__securenowOriginalConsole;
|
|
696
|
+
return (originals && originals[method]) || console[method] || console.log;
|
|
697
|
+
}
|
|
698
|
+
let _lastSuppressedOtlpErrorLogAt = 0;
|
|
699
|
+
function _formatOtlpError(err) {
|
|
700
|
+
if (!err) return 'unknown error';
|
|
701
|
+
const parts = [err.message || String(err)];
|
|
702
|
+
if (err.code && !parts[0].includes(err.code)) parts.push(`code=${err.code}`);
|
|
703
|
+
if (err.syscall) parts.push(`syscall=${err.syscall}`);
|
|
704
|
+
if (err.hostname) parts.push(`host=${err.hostname}`);
|
|
705
|
+
return parts.join(' ');
|
|
706
|
+
}
|
|
707
|
+
function _reportSuppressedOtlpError(kind, err, origin) {
|
|
708
|
+
const level = String(diagLevel || '').toLowerCase();
|
|
709
|
+
if (level === 'none') return;
|
|
710
|
+
const now = Date.now();
|
|
711
|
+
if (level !== 'debug' && now - _lastSuppressedOtlpErrorLogAt < 60_000) return;
|
|
712
|
+
_lastSuppressedOtlpErrorLogAt = now;
|
|
713
|
+
const method = level === 'debug' ? 'debug' : 'error';
|
|
714
|
+
_originalConsole(method).call(
|
|
715
|
+
console,
|
|
716
|
+
'[securenow] OTLP exporter %s suppressed (%s). Telemetry may be missing until the ingest endpoint is reachable: %s',
|
|
717
|
+
kind,
|
|
718
|
+
origin || 'async',
|
|
719
|
+
_formatOtlpError(err)
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
process.on('uncaughtException', (err, origin) => {
|
|
724
|
+
if (_isOtlpTransientError(err) && (_looksLikeOtlpStack(err) || _looksLikeConfiguredOtlpEndpoint(err))) {
|
|
725
|
+
_reportSuppressedOtlpError('error', err, origin);
|
|
726
|
+
return; // swallow - do not crash
|
|
727
|
+
}
|
|
728
|
+
// Not ours — re-throw so the default handler (or the app's own handler) fires
|
|
729
|
+
throw err;
|
|
730
|
+
});
|
|
731
|
+
process.on('unhandledRejection', (reason) => {
|
|
732
|
+
if (_isOtlpTransientError(reason) && (_looksLikeOtlpStack(reason) || _looksLikeConfiguredOtlpEndpoint(reason))) {
|
|
733
|
+
_reportSuppressedOtlpError('rejection', reason, 'unhandledRejection');
|
|
734
|
+
return; // swallow
|
|
735
|
+
}
|
|
736
|
+
// Not ours — re-throw as unhandled so Node's default behaviour applies
|
|
737
|
+
throw reason;
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
// -------- SDK --------
|
|
741
|
+
const traceExporter = new OTLPTraceExporter({ url: tracesUrl, headers });
|
|
742
|
+
const sdk = new NodeSDK({
|
|
743
|
+
...nodeSdkTelemetryOptions,
|
|
744
|
+
traceExporter,
|
|
745
|
+
instrumentations: [
|
|
746
|
+
httpInstrumentation,
|
|
747
|
+
...(disabledMap['@opentelemetry/instrumentation-mongodb'] ? [] : [new MongoDBInstrumentation()]),
|
|
748
|
+
...getNodeAutoInstrumentations({
|
|
749
|
+
...disabledMap,
|
|
750
|
+
'@opentelemetry/instrumentation-http': { enabled: false },
|
|
751
|
+
'@opentelemetry/instrumentation-mongodb': { enabled: false },
|
|
752
|
+
}),
|
|
753
|
+
],
|
|
754
|
+
resource: sharedResource,
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
// -------- start / shutdown (sync/async safe) --------
|
|
758
|
+
(async () => {
|
|
759
|
+
try {
|
|
760
|
+
await Promise.resolve(sdk.start?.());
|
|
761
|
+
console.log('[securenow] OTel SDK started → %s', tracesUrl);
|
|
762
|
+
if (loggingEnabled) {
|
|
763
|
+
console.log('[securenow] 📋 Logging: ENABLED → %s', logsUrl);
|
|
764
|
+
} else {
|
|
765
|
+
console.log('[securenow] Logging: DISABLED (config.logging.enabled=false)');
|
|
766
|
+
}
|
|
767
|
+
if (captureBody) {
|
|
768
|
+
console.log('[securenow] 📠Request body capture: ENABLED (max: %d bytes, redacting %d sensitive fields)', maxBodySize, allSensitiveFields.length);
|
|
769
|
+
}
|
|
770
|
+
if (captureMultipart) {
|
|
771
|
+
console.log('[securenow] 📎 Multipart body capture: ENABLED (streaming — file content not buffered)');
|
|
772
|
+
}
|
|
773
|
+
if (appConfig.boolConfig('runtime.testSpan', false)) {
|
|
774
|
+
const api = require('@opentelemetry/api');
|
|
775
|
+
const tracer = api.trace.getTracer('securenow-smoke');
|
|
776
|
+
const span = tracer.startSpan('securenow.startup.smoke'); span.end();
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Free trial banner
|
|
780
|
+
const { isFreeTrial, patchHttpForBanner } = require('./free-trial-banner');
|
|
781
|
+
if (isFreeTrial(endpointBase) && !appConfig.boolConfig('runtime.hideBanner', false)) {
|
|
782
|
+
patchHttpForBanner();
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Firewall — auto-activates only when a real snk_live_ key is resolvable.
|
|
786
|
+
// resolveApiKey() enforces the prefix, so we skip cleanly when the app has
|
|
787
|
+
// only an app-routing UUID (or nothing at all) — no 401 polling loops.
|
|
788
|
+
const firewallOptions = appConfig.resolveFirewallOptions();
|
|
789
|
+
if (firewallOptions.apiKey) {
|
|
790
|
+
require('./firewall').init({
|
|
791
|
+
apiKey: firewallOptions.apiKey,
|
|
792
|
+
appKey: firewallOptions.appKey,
|
|
793
|
+
environment: firewallOptions.environment,
|
|
794
|
+
apiUrl: firewallOptions.apiUrl,
|
|
795
|
+
apiUrlFallbacks: firewallOptions.apiUrlFallbacks,
|
|
796
|
+
versionCheckInterval: firewallOptions.versionCheckInterval,
|
|
797
|
+
syncInterval: firewallOptions.syncInterval,
|
|
798
|
+
failMode: firewallOptions.failMode,
|
|
799
|
+
statusCode: firewallOptions.statusCode,
|
|
800
|
+
log: firewallOptions.log,
|
|
801
|
+
tcp: firewallOptions.tcp,
|
|
802
|
+
iptables: firewallOptions.iptables,
|
|
803
|
+
cloud: firewallOptions.cloud,
|
|
804
|
+
cloudDryRun: firewallOptions.cloudDryRun,
|
|
805
|
+
cloudflare: firewallOptions.cloudflare,
|
|
806
|
+
aws: firewallOptions.aws,
|
|
807
|
+
gcp: firewallOptions.gcp,
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
} catch (e) {
|
|
811
|
+
console.error('[securenow] OTel start failed:', e && e.stack || e);
|
|
812
|
+
}
|
|
813
|
+
})();
|
|
814
|
+
|
|
815
|
+
let shuttingDown = false;
|
|
816
|
+
async function safeShutdown(sig) {
|
|
817
|
+
if (shuttingDown) return;
|
|
818
|
+
shuttingDown = true;
|
|
819
|
+
try {
|
|
820
|
+
await Promise.resolve(sdk.shutdown?.());
|
|
821
|
+
if (loggerProvider) {
|
|
822
|
+
await Promise.resolve(loggerProvider.shutdown?.());
|
|
823
|
+
}
|
|
824
|
+
try { require('./firewall').shutdown(); } catch (_) {}
|
|
825
|
+
console.log(`[securenow] Tracing and logging terminated on ${sig}`);
|
|
826
|
+
}
|
|
827
|
+
catch (e) { console.error('[securenow] Shutdown error:', e); }
|
|
828
|
+
finally { process.exit(0); }
|
|
829
|
+
}
|
|
830
|
+
process.on('SIGINT', () => safeShutdown('SIGINT'));
|
|
831
|
+
process.on('SIGTERM', () => safeShutdown('SIGTERM'));
|
|
832
|
+
|
|
833
|
+
// -------- Export logger for consuming applications --------
|
|
834
|
+
module.exports = {
|
|
835
|
+
loggerProvider,
|
|
836
|
+
getLogger: (name = 'default', version = '1.0.0') => {
|
|
837
|
+
if (!loggerProvider) {
|
|
838
|
+
console.warn('[securenow] Logging is disabled (config.logging.enabled=false). Enable it in .securenow/credentials.json to use getLogger().');
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
841
|
+
return loggerProvider.getLogger(name, version);
|
|
842
|
+
},
|
|
843
|
+
isLoggingEnabled: () => loggingEnabled,
|
|
844
|
+
};
|