securenow 7.7.10 → 7.7.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NPM_README.md +4 -2
- package/README.md +2 -0
- package/SKILL-CLI.md +5 -4
- package/cli/apiKey.js +43 -1
- package/cli/config.js +9 -3
- package/cli.js +15 -5
- package/free-trial-banner.js +2 -1
- package/nextjs.js +14 -5
- package/package.json +1 -1
- package/tracing.js +146 -102
package/NPM_README.md
CHANGED
|
@@ -157,7 +157,8 @@ npx securenow login --global
|
|
|
157
157
|
# Check who you're logged in as (shows auth source)
|
|
158
158
|
npx securenow whoami
|
|
159
159
|
|
|
160
|
-
#
|
|
160
|
+
# Need or already have a firewall key? Create or store it without re-running login:
|
|
161
|
+
npx securenow api-key create --name "CLI firewall"
|
|
161
162
|
npx securenow api-key set snk_live_abc123... # --global for ~/.securenow/
|
|
162
163
|
npx securenow api-key show # masked key + source
|
|
163
164
|
npx securenow api-key clear # remove just the key
|
|
@@ -491,7 +492,8 @@ npx securenow logs --json --level error | jq '.logs'
|
|
|
491
492
|
| | `apps info <id>` | Application details |
|
|
492
493
|
| | `apps delete <id>` | Delete application |
|
|
493
494
|
| | `apps default <key>` | Set default app |
|
|
494
|
-
| **API Key** | `api-key
|
|
495
|
+
| **API Key** | `api-key create [--name "CLI firewall"] [--global]` | Mint and save a firewall key with your logged-in session |
|
|
496
|
+
| | `api-key set <snk_live_...> [--global]` | Save firewall key to `.securenow/credentials.json` |
|
|
495
497
|
| | `api-key show` | Show masked key + source |
|
|
496
498
|
| | `api-key clear [--global]` | Remove stored key (leaves session/app) |
|
|
497
499
|
| **Observe** | `traces` | List traces |
|
package/README.md
CHANGED
|
@@ -175,6 +175,7 @@ npx securenow login --global # save to ~/.securenow/ instead
|
|
|
175
175
|
npx securenow login --token <TOKEN> # headless (CI)
|
|
176
176
|
npx securenow init --env local # scaffold framework files + local env scope
|
|
177
177
|
npx securenow credentials runtime --env production # write tokenless production credentials file
|
|
178
|
+
npx securenow api-key create --name "CLI firewall" # mint + store firewall key
|
|
178
179
|
npx securenow api-key set snk_live_... # store firewall key in .securenow/credentials.json
|
|
179
180
|
|
|
180
181
|
# Apps
|
|
@@ -301,6 +302,7 @@ After install, the `securenow` CLI is available via `npx securenow` or globally
|
|
|
301
302
|
| `securenow logout` | Clear project-local credentials |
|
|
302
303
|
| `securenow logout --global` | Clear ~/.securenow/ instead |
|
|
303
304
|
| `securenow whoami` | Show current session (email, app, expiry) |
|
|
305
|
+
| `securenow api-key create [--name "CLI firewall"]` | Mint and store a firewall key using your session token |
|
|
304
306
|
| `securenow api-key set <snk_live_...>` | Store firewall key in `.securenow/credentials.json` (`--global` for `~/.securenow/`) |
|
|
305
307
|
| `securenow api-key show` | Print masked key + source file |
|
|
306
308
|
| `securenow api-key clear` | Remove stored key (`--global` for `~/.securenow/`) |
|
package/SKILL-CLI.md
CHANGED
|
@@ -153,16 +153,17 @@ securenow apps scan [--yes] # scan all app domains for new subd
|
|
|
153
153
|
|
|
154
154
|
Manage the firewall API key stored in the credentials file. Since v7.5.1 the login flow writes `snk_live_...` keys to `.securenow/credentials.json` by default, so no env var is required for local dev.
|
|
155
155
|
|
|
156
|
-
```bash
|
|
157
|
-
securenow api-key
|
|
158
|
-
securenow api-key set
|
|
156
|
+
```bash
|
|
157
|
+
securenow api-key create --name "CLI firewall" # mint + save a firewall key with your logged-in session
|
|
158
|
+
securenow api-key set snk_live_xxxxxxxxxx # save to project ./.securenow/ (default)
|
|
159
|
+
securenow api-key set snk_live_xxx --global # save to ~/.securenow/ instead
|
|
159
160
|
securenow api-key show # print the masked current key + its source
|
|
160
161
|
securenow api-key clear # remove just the API key (keeps session/app)
|
|
161
162
|
securenow api-key clear --global # same, but from the global file
|
|
162
163
|
securenow credentials runtime --env production # write .securenow/credentials.production.json for production secret-file deploys
|
|
163
164
|
```
|
|
164
165
|
|
|
165
|
-
The key must start with `snk_live_`. `securenow login` with firewall enabled writes the key automatically; use `api-key set` when you already have a key from the dashboard
|
|
166
|
+
The key must start with `snk_live_`. `securenow login` with firewall enabled writes the key automatically; use `api-key create` when you have an account session but no runtime key yet, or `api-key set` when you already have a key from the dashboard.
|
|
166
167
|
|
|
167
168
|
### Init — Project Setup
|
|
168
169
|
|
package/cli/apiKey.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { api, requireAuth } = require('./client');
|
|
3
4
|
const config = require('./config');
|
|
4
5
|
const ui = require('./ui');
|
|
5
6
|
|
|
@@ -8,6 +9,47 @@ function maskKey(key) {
|
|
|
8
9
|
return `${key.slice(0, 12)}••••••${key.slice(-4)}`;
|
|
9
10
|
}
|
|
10
11
|
|
|
12
|
+
async function create(args, flags) {
|
|
13
|
+
requireAuth();
|
|
14
|
+
|
|
15
|
+
const name = flags.name || args.join(' ').trim() || 'CLI firewall';
|
|
16
|
+
const preset = flags.preset || 'firewall';
|
|
17
|
+
const local = flags.global ? false : true;
|
|
18
|
+
|
|
19
|
+
const s = ui.spinner(`Creating ${preset} API key`);
|
|
20
|
+
try {
|
|
21
|
+
const result = await api.post('/api-keys', { name, preset });
|
|
22
|
+
const key = result && result.key;
|
|
23
|
+
if (!key || !key.startsWith('snk_live_')) {
|
|
24
|
+
throw new Error('API did not return a valid firewall key');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
config.setApiKey(key, { local });
|
|
28
|
+
if (local) config.ensureLocalGitignore();
|
|
29
|
+
s.stop('API key created and saved');
|
|
30
|
+
|
|
31
|
+
if (flags.json) {
|
|
32
|
+
ui.json({
|
|
33
|
+
id: result.apiKey?._id || result.apiKey?.id || null,
|
|
34
|
+
name: result.apiKey?.name || name,
|
|
35
|
+
preset,
|
|
36
|
+
key: maskKey(key),
|
|
37
|
+
savedTo: local ? 'project .securenow/credentials.json' : '~/.securenow/credentials.json',
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
ui.success(`API key saved (${maskKey(key)})`);
|
|
43
|
+
ui.info(local
|
|
44
|
+
? 'Stored in project .securenow/credentials.json (local)'
|
|
45
|
+
: 'Stored in ~/.securenow/credentials.json (global)');
|
|
46
|
+
ui.info('The plaintext key is not printed. The SDK will read it from the credentials file.');
|
|
47
|
+
} catch (err) {
|
|
48
|
+
s.fail('Failed to create API key');
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
11
53
|
async function set(args, flags) {
|
|
12
54
|
const key = args[0];
|
|
13
55
|
if (!key) {
|
|
@@ -52,4 +94,4 @@ async function show() {
|
|
|
52
94
|
ui.info(`Source: ${config.getAuthSource()}`);
|
|
53
95
|
}
|
|
54
96
|
|
|
55
|
-
module.exports = { set, clear, show };
|
|
97
|
+
module.exports = { create, set, clear, show };
|
package/cli/config.js
CHANGED
|
@@ -47,6 +47,12 @@ function saveJSON(filepath, data) {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function credentialsForWrite(targetFile) {
|
|
51
|
+
const inherited = loadCredentials() || {};
|
|
52
|
+
const existing = loadJSON(targetFile);
|
|
53
|
+
return appConfig.mergeCredentials(inherited, existing) || {};
|
|
54
|
+
}
|
|
55
|
+
|
|
50
56
|
function credentialsFileForLocal(local) {
|
|
51
57
|
return local ? LOCAL_CREDENTIALS_FILE : CREDENTIALS_FILE;
|
|
52
58
|
}
|
|
@@ -113,7 +119,7 @@ function withOnboardingFirewallEnabled(creds) {
|
|
|
113
119
|
function ensureCredentialDefaults({ local, enableFirewall = false } = {}) {
|
|
114
120
|
const useLocal = local === true || (local == null && hasLocalCredentials());
|
|
115
121
|
const targetFile = credentialsFileForLocal(useLocal);
|
|
116
|
-
const existing = loadJSON(targetFile);
|
|
122
|
+
const existing = useLocal ? credentialsForWrite(targetFile) : loadJSON(targetFile);
|
|
117
123
|
const payload = enableFirewall
|
|
118
124
|
? withOnboardingFirewallEnabled(existing || {})
|
|
119
125
|
: appConfig.withCredentialDefaults(existing || {}) || {};
|
|
@@ -168,7 +174,7 @@ function getApp() {
|
|
|
168
174
|
function setApiKey(apiKey, { local } = {}) {
|
|
169
175
|
const useLocal = local === true || (local == null && hasLocalCredentials());
|
|
170
176
|
const targetFile = credentialsFileForLocal(useLocal);
|
|
171
|
-
const existing = loadJSON(targetFile);
|
|
177
|
+
const existing = useLocal ? credentialsForWrite(targetFile) : loadJSON(targetFile);
|
|
172
178
|
saveJSON(targetFile, withOnboardingFirewallEnabled({ ...existing, apiKey }));
|
|
173
179
|
}
|
|
174
180
|
|
|
@@ -189,7 +195,7 @@ function getApiKey() {
|
|
|
189
195
|
function setApp(app, { local } = {}) {
|
|
190
196
|
const useLocal = local === true || (local == null && hasLocalCredentials());
|
|
191
197
|
const targetFile = credentialsFileForLocal(useLocal);
|
|
192
|
-
const existing = loadJSON(targetFile);
|
|
198
|
+
const existing = useLocal ? credentialsForWrite(targetFile) : loadJSON(targetFile);
|
|
193
199
|
saveJSON(targetFile, appConfig.withCredentialDefaults({
|
|
194
200
|
...existing,
|
|
195
201
|
app: {
|
package/cli.js
CHANGED
|
@@ -77,11 +77,21 @@ const COMMANDS = {
|
|
|
77
77
|
},
|
|
78
78
|
'api-key': {
|
|
79
79
|
desc: 'Manage the firewall API key stored in .securenow/credentials.json',
|
|
80
|
-
usage: 'securenow api-key <subcommand> [options]',
|
|
81
|
-
sub: {
|
|
82
|
-
|
|
83
|
-
desc: '
|
|
84
|
-
usage: 'securenow api-key
|
|
80
|
+
usage: 'securenow api-key <subcommand> [options]',
|
|
81
|
+
sub: {
|
|
82
|
+
create: {
|
|
83
|
+
desc: 'Create a firewall API key with your session token and save it',
|
|
84
|
+
usage: 'securenow api-key create [name] [--name <name>] [--preset firewall] [--global]',
|
|
85
|
+
flags: {
|
|
86
|
+
name: 'Human-readable key name',
|
|
87
|
+
preset: 'API key preset to create (default: firewall)',
|
|
88
|
+
global: 'Save to ~/.securenow/ instead of project-local',
|
|
89
|
+
},
|
|
90
|
+
run: (a, f) => require('./cli/apiKey').create(a, f),
|
|
91
|
+
},
|
|
92
|
+
set: {
|
|
93
|
+
desc: 'Save an API key (snk_live_...) to the credentials file',
|
|
94
|
+
usage: 'securenow api-key set <snk_live_...> [--global]',
|
|
85
95
|
flags: { global: 'Save to ~/.securenow/ instead of project-local' },
|
|
86
96
|
run: (a, f) => require('./cli/apiKey').set(a, f),
|
|
87
97
|
},
|
package/free-trial-banner.js
CHANGED
|
@@ -97,7 +97,8 @@ var BANNER_SCRIPT =
|
|
|
97
97
|
*/
|
|
98
98
|
function patchHttpForBanner() {
|
|
99
99
|
try {
|
|
100
|
-
var
|
|
100
|
+
var nodeRequire = typeof __non_webpack_require__ === 'function' ? __non_webpack_require__ : eval('require');
|
|
101
|
+
var http = nodeRequire('node:http');
|
|
101
102
|
var _origWrite = http.ServerResponse.prototype.write;
|
|
102
103
|
var _origEnd = http.ServerResponse.prototype.end;
|
|
103
104
|
|
package/nextjs.js
CHANGED
|
@@ -34,6 +34,15 @@ const env = appConfig.env;
|
|
|
34
34
|
|
|
35
35
|
let isRegistered = false;
|
|
36
36
|
|
|
37
|
+
function requireRuntimeModule(name) {
|
|
38
|
+
const nodeRequire = typeof __non_webpack_require__ === 'function' ? __non_webpack_require__ : eval('require');
|
|
39
|
+
return nodeRequire(name);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function requireNodeBuiltin(name) {
|
|
43
|
+
return requireRuntimeModule(name);
|
|
44
|
+
}
|
|
45
|
+
|
|
37
46
|
function createResource(attributes) {
|
|
38
47
|
if (typeof otelResources.resourceFromAttributes === 'function') {
|
|
39
48
|
return otelResources.resourceFromAttributes(attributes);
|
|
@@ -501,7 +510,7 @@ function registerSecureNow(options = {}) {
|
|
|
501
510
|
|
|
502
511
|
// Auto-log every incoming HTTP request/response
|
|
503
512
|
try {
|
|
504
|
-
const http =
|
|
513
|
+
const http = requireNodeBuiltin('node:http');
|
|
505
514
|
const originalEmit = http.Server.prototype.emit;
|
|
506
515
|
http.Server.prototype.emit = function (event, req, res) {
|
|
507
516
|
if (event === 'request' && req && res) {
|
|
@@ -549,8 +558,8 @@ function registerSecureNow(options = {}) {
|
|
|
549
558
|
} catch (_) {}
|
|
550
559
|
|
|
551
560
|
// Graceful shutdown for logs
|
|
552
|
-
process.on('SIGTERM', async () => { try { await loggerProvider.shutdown(); } catch (_) {} try {
|
|
553
|
-
process.on('SIGINT', async () => { try { await loggerProvider.shutdown(); } catch (_) {} try {
|
|
561
|
+
process.on('SIGTERM', async () => { try { await loggerProvider.shutdown(); } catch (_) {} try { requireRuntimeModule('./firewall').shutdown(); } catch (_) {} });
|
|
562
|
+
process.on('SIGINT', async () => { try { await loggerProvider.shutdown(); } catch (_) {} try { requireRuntimeModule('./firewall').shutdown(); } catch (_) {} });
|
|
554
563
|
} catch (e) {
|
|
555
564
|
console.warn('[securenow] ⚠️ Logging setup failed (missing @opentelemetry/exporter-logs-otlp-http or @opentelemetry/sdk-logs):', e.message);
|
|
556
565
|
}
|
|
@@ -563,7 +572,7 @@ function registerSecureNow(options = {}) {
|
|
|
563
572
|
|
|
564
573
|
// Free trial banner (optional — may not be bundled in standalone builds)
|
|
565
574
|
try {
|
|
566
|
-
const { isFreeTrial, patchHttpForBanner } =
|
|
575
|
+
const { isFreeTrial, patchHttpForBanner } = requireRuntimeModule('./free-trial-banner');
|
|
567
576
|
if (isFreeTrial(endpointBase) && String(env('SECURENOW_HIDE_BANNER')) !== '1') {
|
|
568
577
|
patchHttpForBanner();
|
|
569
578
|
}
|
|
@@ -607,7 +616,7 @@ function registerSecureNow(options = {}) {
|
|
|
607
616
|
const firewallOptions = appConfig.resolveFirewallOptions();
|
|
608
617
|
if (firewallOptions.apiKey && firewallOptions.enabled) {
|
|
609
618
|
try {
|
|
610
|
-
|
|
619
|
+
requireRuntimeModule('./firewall').init({
|
|
611
620
|
apiKey: firewallOptions.apiKey,
|
|
612
621
|
appKey: firewallOptions.appKey,
|
|
613
622
|
environment: deploymentEnvironment || firewallOptions.environment,
|
package/package.json
CHANGED
package/tracing.js
CHANGED
|
@@ -124,9 +124,9 @@ function extractBoundary(contentType) {
|
|
|
124
124
|
return match ? (match[1] || match[2]) : null;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
function
|
|
127
|
+
function createMultipartMetaCollector(contentType, sensitiveFields, maxTextFieldSize, onComplete) {
|
|
128
128
|
const boundary = extractBoundary(contentType);
|
|
129
|
-
if (!boundary) { onComplete({ error: 'BOUNDARY_NOT_FOUND' }); return; }
|
|
129
|
+
if (!boundary) { onComplete({ error: 'BOUNDARY_NOT_FOUND' }); return null; }
|
|
130
130
|
|
|
131
131
|
const result = { fields: Object.create(null), files: [] };
|
|
132
132
|
let totalSize = 0;
|
|
@@ -219,13 +219,14 @@ function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFiel
|
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
222
|
+
function onData(chunk) {
|
|
223
|
+
const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
224
|
+
totalSize += data.length;
|
|
225
|
+
buf = Buffer.concat([buf, data]);
|
|
225
226
|
drain();
|
|
226
|
-
}
|
|
227
|
+
}
|
|
227
228
|
|
|
228
|
-
|
|
229
|
+
function onEnd() {
|
|
229
230
|
try {
|
|
230
231
|
if (!inHeaders && fldName) {
|
|
231
232
|
bodyBytes += buf.length;
|
|
@@ -236,7 +237,16 @@ function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFiel
|
|
|
236
237
|
} catch (e) {
|
|
237
238
|
onComplete({ error: 'PARSE_ERROR' });
|
|
238
239
|
}
|
|
239
|
-
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return { onData, onEnd };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFieldSize, onComplete) {
|
|
246
|
+
const collector = createMultipartMetaCollector(contentType, sensitiveFields, maxTextFieldSize, onComplete);
|
|
247
|
+
if (!collector) return;
|
|
248
|
+
request.on('data', collector.onData);
|
|
249
|
+
request.on('end', collector.onEnd);
|
|
240
250
|
}
|
|
241
251
|
|
|
242
252
|
// -------- ESM detection --------
|
|
@@ -342,6 +352,133 @@ const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveField
|
|
|
342
352
|
|
|
343
353
|
const captureMultipart = !/^(0|false)$/i.test(String(env('SECURENOW_CAPTURE_MULTIPART') ?? ''));
|
|
344
354
|
|
|
355
|
+
const BODY_CAPTURE_PATCH = Symbol.for('securenow.bodyCapture.emitPatch');
|
|
356
|
+
|
|
357
|
+
function installRequestBodyObserver(span, request, contentType) {
|
|
358
|
+
if (!request || request[BODY_CAPTURE_PATCH]) return;
|
|
359
|
+
|
|
360
|
+
const normalizedContentType = String(contentType || '').toLowerCase();
|
|
361
|
+
const isStructuredBody = captureBody && (
|
|
362
|
+
normalizedContentType.includes('application/json') ||
|
|
363
|
+
normalizedContentType.includes('application/graphql') ||
|
|
364
|
+
normalizedContentType.includes('application/x-www-form-urlencoded')
|
|
365
|
+
);
|
|
366
|
+
const isMultipartBody = normalizedContentType.includes('multipart/form-data');
|
|
367
|
+
|
|
368
|
+
if (!isStructuredBody && !isMultipartBody) return;
|
|
369
|
+
|
|
370
|
+
if (isMultipartBody && !captureMultipart) {
|
|
371
|
+
span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
|
|
372
|
+
span.setAttribute('http.request.body.type', 'multipart');
|
|
373
|
+
span.setAttribute('http.request.body.note', 'Multipart capture disabled (SECURENOW_CAPTURE_MULTIPART=0)');
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
let chunks = [];
|
|
378
|
+
let size = 0;
|
|
379
|
+
let structuredDone = false;
|
|
380
|
+
|
|
381
|
+
const multipartCollector = isMultipartBody && captureMultipart
|
|
382
|
+
? createMultipartMetaCollector(normalizedContentType, allSensitiveFields, 1000, ({ error, parsed, totalSize }) => {
|
|
383
|
+
try {
|
|
384
|
+
if (error === 'BOUNDARY_NOT_FOUND') {
|
|
385
|
+
span.setAttribute('http.request.body', '[MULTIPART - BOUNDARY NOT FOUND]');
|
|
386
|
+
span.setAttribute('http.request.body.type', 'multipart');
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (error) {
|
|
390
|
+
span.setAttribute('http.request.body', '[MULTIPART - PARSE ERROR]');
|
|
391
|
+
span.setAttribute('http.request.body.type', 'multipart');
|
|
392
|
+
span.setAttribute('http.request.body.parse_error', true);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
span.setAttributes({
|
|
396
|
+
'http.request.body': JSON.stringify(parsed).substring(0, maxBodySize),
|
|
397
|
+
'http.request.body.type': 'multipart',
|
|
398
|
+
'http.request.body.size': totalSize,
|
|
399
|
+
'http.request.body.fields_count': Object.keys(parsed.fields).length,
|
|
400
|
+
'http.request.body.files_count': parsed.files.length,
|
|
401
|
+
});
|
|
402
|
+
} catch (e) {
|
|
403
|
+
span.setAttribute('http.request.body', '[MULTIPART - PARSE ERROR]');
|
|
404
|
+
span.setAttribute('http.request.body.type', 'multipart');
|
|
405
|
+
span.setAttribute('http.request.body.parse_error', true);
|
|
406
|
+
}
|
|
407
|
+
})
|
|
408
|
+
: null;
|
|
409
|
+
|
|
410
|
+
if (isMultipartBody && captureMultipart && !multipartCollector) return;
|
|
411
|
+
|
|
412
|
+
function finishStructuredCapture() {
|
|
413
|
+
if (!isStructuredBody || structuredDone) return;
|
|
414
|
+
structuredDone = true;
|
|
415
|
+
|
|
416
|
+
if (size > maxBodySize) {
|
|
417
|
+
span.setAttribute('http.request.body', `[TOO LARGE: ${size} bytes]`);
|
|
418
|
+
span.setAttribute('http.request.body.size', size);
|
|
419
|
+
chunks = [];
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (chunks.length === 0) return;
|
|
424
|
+
|
|
425
|
+
const body = Buffer.concat(chunks).toString('utf8');
|
|
426
|
+
chunks = [];
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
if (normalizedContentType.includes('application/graphql')) {
|
|
430
|
+
const redacted = redactGraphQLQuery(body, allSensitiveFields);
|
|
431
|
+
span.setAttributes({
|
|
432
|
+
'http.request.body': redacted.substring(0, maxBodySize),
|
|
433
|
+
'http.request.body.type': 'graphql',
|
|
434
|
+
'http.request.body.size': size,
|
|
435
|
+
});
|
|
436
|
+
} else if (normalizedContentType.includes('application/json')) {
|
|
437
|
+
const parsed = JSON.parse(body);
|
|
438
|
+
const redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
439
|
+
span.setAttributes({
|
|
440
|
+
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
441
|
+
'http.request.body.type': 'json',
|
|
442
|
+
'http.request.body.size': size,
|
|
443
|
+
});
|
|
444
|
+
} else if (normalizedContentType.includes('application/x-www-form-urlencoded')) {
|
|
445
|
+
const parsed = Object.fromEntries(new URLSearchParams(body));
|
|
446
|
+
const redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
447
|
+
span.setAttributes({
|
|
448
|
+
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
449
|
+
'http.request.body.type': 'form',
|
|
450
|
+
'http.request.body.size': size,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
} catch (e) {
|
|
454
|
+
span.setAttribute('http.request.body', '[UNPARSEABLE - REDACTED FOR SAFETY]');
|
|
455
|
+
span.setAttribute('http.request.body.parse_error', true);
|
|
456
|
+
span.setAttribute('http.request.body.size', size);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const originalEmit = request.emit;
|
|
461
|
+
request[BODY_CAPTURE_PATCH] = true;
|
|
462
|
+
request.emit = function securenowObservedEmit(event, ...args) {
|
|
463
|
+
try {
|
|
464
|
+
if (event === 'data' && args.length > 0) {
|
|
465
|
+
const chunk = Buffer.isBuffer(args[0]) ? args[0] : Buffer.from(args[0]);
|
|
466
|
+
if (isStructuredBody) {
|
|
467
|
+
size += chunk.length;
|
|
468
|
+
if (size <= maxBodySize) chunks.push(chunk);
|
|
469
|
+
}
|
|
470
|
+
if (multipartCollector) multipartCollector.onData(chunk);
|
|
471
|
+
} else if (event === 'end') {
|
|
472
|
+
finishStructuredCapture();
|
|
473
|
+
if (multipartCollector) multipartCollector.onEnd();
|
|
474
|
+
}
|
|
475
|
+
} catch (_) {
|
|
476
|
+
// Body capture must never interfere with the application request stream.
|
|
477
|
+
}
|
|
478
|
+
return originalEmit.apply(this, [event, ...args]);
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
345
482
|
// -------- Trusted proxy IP resolution --------
|
|
346
483
|
const { resolveClientIpWithDetails } = require('./resolve-ip');
|
|
347
484
|
|
|
@@ -391,100 +528,7 @@ const httpInstrumentation = new HttpInstrumentation({
|
|
|
391
528
|
|
|
392
529
|
if ((captureBody || captureMultipart) && request.method && ['POST', 'PUT', 'PATCH'].includes(request.method)) {
|
|
393
530
|
const contentType = request.headers['content-type'] || '';
|
|
394
|
-
|
|
395
|
-
if (captureBody && (contentType.includes('application/json') ||
|
|
396
|
-
contentType.includes('application/graphql') ||
|
|
397
|
-
contentType.includes('application/x-www-form-urlencoded'))) {
|
|
398
|
-
|
|
399
|
-
let body = '';
|
|
400
|
-
const chunks = [];
|
|
401
|
-
let size = 0;
|
|
402
|
-
|
|
403
|
-
request.on('data', (chunk) => {
|
|
404
|
-
size += chunk.length;
|
|
405
|
-
if (size <= maxBodySize) {
|
|
406
|
-
chunks.push(chunk);
|
|
407
|
-
}
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
request.on('end', () => {
|
|
411
|
-
if (size <= maxBodySize && chunks.length > 0) {
|
|
412
|
-
body = Buffer.concat(chunks).toString('utf8');
|
|
413
|
-
|
|
414
|
-
try {
|
|
415
|
-
let redacted;
|
|
416
|
-
|
|
417
|
-
if (contentType.includes('application/graphql')) {
|
|
418
|
-
// GraphQL: redact query string
|
|
419
|
-
redacted = redactGraphQLQuery(body, allSensitiveFields);
|
|
420
|
-
span.setAttributes({
|
|
421
|
-
'http.request.body': redacted.substring(0, maxBodySize),
|
|
422
|
-
'http.request.body.type': 'graphql',
|
|
423
|
-
'http.request.body.size': size,
|
|
424
|
-
});
|
|
425
|
-
} else if (contentType.includes('application/json')) {
|
|
426
|
-
// JSON: parse and redact object
|
|
427
|
-
const parsed = JSON.parse(body);
|
|
428
|
-
redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
429
|
-
span.setAttributes({
|
|
430
|
-
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
431
|
-
'http.request.body.type': 'json',
|
|
432
|
-
'http.request.body.size': size,
|
|
433
|
-
});
|
|
434
|
-
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
435
|
-
// Form: parse and redact
|
|
436
|
-
const parsed = Object.fromEntries(new URLSearchParams(body));
|
|
437
|
-
redacted = redactSensitiveData(parsed, allSensitiveFields);
|
|
438
|
-
span.setAttributes({
|
|
439
|
-
'http.request.body': JSON.stringify(redacted).substring(0, maxBodySize),
|
|
440
|
-
'http.request.body.type': 'form',
|
|
441
|
-
'http.request.body.size': size,
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
} catch (e) {
|
|
445
|
-
span.setAttribute('http.request.body', '[UNPARSEABLE - REDACTED FOR SAFETY]');
|
|
446
|
-
span.setAttribute('http.request.body.parse_error', true);
|
|
447
|
-
span.setAttribute('http.request.body.size', size);
|
|
448
|
-
}
|
|
449
|
-
} else if (size > maxBodySize) {
|
|
450
|
-
span.setAttribute('http.request.body', `[TOO LARGE: ${size} bytes]`);
|
|
451
|
-
span.setAttribute('http.request.body.size', size);
|
|
452
|
-
}
|
|
453
|
-
});
|
|
454
|
-
} else if (contentType.includes('multipart/form-data')) {
|
|
455
|
-
if (captureMultipart) {
|
|
456
|
-
collectMultipartMeta(request, contentType, allSensitiveFields, 1000, ({ error, parsed, totalSize }) => {
|
|
457
|
-
try {
|
|
458
|
-
if (error === 'BOUNDARY_NOT_FOUND') {
|
|
459
|
-
span.setAttribute('http.request.body', '[MULTIPART - BOUNDARY NOT FOUND]');
|
|
460
|
-
span.setAttribute('http.request.body.type', 'multipart');
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
if (error) {
|
|
464
|
-
span.setAttribute('http.request.body', '[MULTIPART - PARSE ERROR]');
|
|
465
|
-
span.setAttribute('http.request.body.type', 'multipart');
|
|
466
|
-
span.setAttribute('http.request.body.parse_error', true);
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
469
|
-
span.setAttributes({
|
|
470
|
-
'http.request.body': JSON.stringify(parsed).substring(0, maxBodySize),
|
|
471
|
-
'http.request.body.type': 'multipart',
|
|
472
|
-
'http.request.body.size': totalSize,
|
|
473
|
-
'http.request.body.fields_count': Object.keys(parsed.fields).length,
|
|
474
|
-
'http.request.body.files_count': parsed.files.length,
|
|
475
|
-
});
|
|
476
|
-
} catch (e) {
|
|
477
|
-
span.setAttribute('http.request.body', '[MULTIPART - PARSE ERROR]');
|
|
478
|
-
span.setAttribute('http.request.body.type', 'multipart');
|
|
479
|
-
span.setAttribute('http.request.body.parse_error', true);
|
|
480
|
-
}
|
|
481
|
-
});
|
|
482
|
-
} else {
|
|
483
|
-
span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
|
|
484
|
-
span.setAttribute('http.request.body.type', 'multipart');
|
|
485
|
-
span.setAttribute('http.request.body.note', 'Multipart capture disabled (SECURENOW_CAPTURE_MULTIPART=0)');
|
|
486
|
-
}
|
|
487
|
-
}
|
|
531
|
+
installRequestBodyObserver(span, request, contentType);
|
|
488
532
|
}
|
|
489
533
|
} catch (error) {
|
|
490
534
|
// Silently fail
|