securenow 8.12.1 → 8.12.2
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/README.md +2 -0
- package/cli/automation.js +4 -2
- package/cli/ui.js +18 -1
- package/console-instrumentation.js +1 -12
- package/events.js +1 -0
- package/firewall-only.js +2 -0
- package/firewall-paths.js +46 -0
- package/firewall.js +2 -10
- package/log-format.js +28 -0
- package/nextjs.js +1 -0
- package/package.json +5 -2
- package/register.js +2 -0
- package/sdk-disabled.js +18 -0
- package/sessions.js +2 -0
- package/tracing.js +3 -1
package/README.md
CHANGED
|
@@ -111,6 +111,8 @@ npx securenow doctor # diagnose config + connectivity
|
|
|
111
111
|
npx securenow status # dashboard summary
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
+
**Tests** — test runs otherwise report to your real app with the project's `.securenow` credentials. `SECURENOW_DISABLED=1 npm test` turns the SDK off for that process: no traces, logs, events, firewall or session sync, and `guard()` just calls `next()`. This is the only setting read from the environment.
|
|
115
|
+
|
|
114
116
|
---
|
|
115
117
|
|
|
116
118
|
## Connect your coding agent (MCP)
|
package/cli/automation.js
CHANGED
|
@@ -198,18 +198,20 @@ async function dryRun(args, flags) {
|
|
|
198
198
|
ui.keyValue([
|
|
199
199
|
['Scanned IPs', String(results.scanned ?? 0)],
|
|
200
200
|
['Matched IPs', String(results.matched ?? 0)],
|
|
201
|
+
...(results.testModeMatched != null ? [['Test-mode matches', `${results.testModeMatched} (preview only: never acted on until the rule is promoted)`]] : []),
|
|
201
202
|
['Sample count', String((results.samples || []).length)],
|
|
202
203
|
]);
|
|
203
204
|
if ((results.samples || []).length) {
|
|
204
205
|
console.log('');
|
|
205
206
|
const rows = results.samples.map((sample) => [
|
|
206
207
|
sample.ip,
|
|
208
|
+
ui.truncate([].concat(sample.alertName || []).slice(-1).join(''), 40),
|
|
207
209
|
ui.truncate((sample.path || []).join(', '), 32),
|
|
208
|
-
ui.truncate((sample.attackType || []).join(', '), 28),
|
|
209
210
|
(sample.environment || []).join(', ') || '-',
|
|
210
211
|
String(sample.riskScore ?? '-'),
|
|
212
|
+
sample.testMode ? 'test' : 'prod',
|
|
211
213
|
]);
|
|
212
|
-
ui.table(['IP', '
|
|
214
|
+
ui.table(['IP', 'Alert', 'Paths', 'Env', 'Risk', 'Mode'], rows);
|
|
213
215
|
}
|
|
214
216
|
console.log('');
|
|
215
217
|
} catch (err) {
|
package/cli/ui.js
CHANGED
|
@@ -182,10 +182,19 @@ function prompt(question, opts = {}) {
|
|
|
182
182
|
|
|
183
183
|
return new Promise((resolve) => {
|
|
184
184
|
const q = opts.secret ? question : ` ${c.cyan('?')} ${question} `;
|
|
185
|
+
let answered = false;
|
|
185
186
|
rl.question(q, (answer) => {
|
|
187
|
+
answered = true;
|
|
186
188
|
rl.close();
|
|
187
189
|
resolve(answer.trim());
|
|
188
190
|
});
|
|
191
|
+
// stdin closed without an answer (EOF, /dev/null, a coding agent's shell): resolve empty instead of
|
|
192
|
+
// leaving the promise pending until the process silently exits.
|
|
193
|
+
rl.once('close', () => {
|
|
194
|
+
if (answered) return;
|
|
195
|
+
process.stderr.write('\n');
|
|
196
|
+
resolve('');
|
|
197
|
+
});
|
|
189
198
|
|
|
190
199
|
if (opts.secret) {
|
|
191
200
|
const onData = (char) => {
|
|
@@ -202,7 +211,15 @@ function prompt(question, opts = {}) {
|
|
|
202
211
|
async function confirm(question, defaultYes = false) {
|
|
203
212
|
const hint = defaultYes ? 'Y/n' : 'y/N';
|
|
204
213
|
const answer = await prompt(`${question} ${c.dim(`(${hint})`)}`);
|
|
205
|
-
if (!answer)
|
|
214
|
+
if (!answer) {
|
|
215
|
+
// A script or coding agent has no terminal to answer from: say so and exit non-zero instead of
|
|
216
|
+
// silently cancelling the write with exit code 0.
|
|
217
|
+
if (!defaultYes && !process.stdin.isTTY) {
|
|
218
|
+
error('No confirmation received (non-interactive shell). Re-run with --yes to confirm.');
|
|
219
|
+
process.exitCode = 1;
|
|
220
|
+
}
|
|
221
|
+
return defaultYes;
|
|
222
|
+
}
|
|
206
223
|
return answer.toLowerCase().startsWith('y');
|
|
207
224
|
}
|
|
208
225
|
|
|
@@ -64,18 +64,7 @@ const SeverityNumber = {
|
|
|
64
64
|
* Format arguments into a log message
|
|
65
65
|
*/
|
|
66
66
|
function formatMessage(args) {
|
|
67
|
-
return args
|
|
68
|
-
.map((arg) => {
|
|
69
|
-
if (typeof arg === 'object' && arg !== null) {
|
|
70
|
-
try {
|
|
71
|
-
return JSON.stringify(arg);
|
|
72
|
-
} catch (e) {
|
|
73
|
-
return String(arg);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return String(arg);
|
|
77
|
-
})
|
|
78
|
-
.join(' ');
|
|
67
|
+
return require('./log-format').formatLogArgs(args);
|
|
79
68
|
}
|
|
80
69
|
|
|
81
70
|
const { context, trace } = require('@opentelemetry/api');
|
package/events.js
CHANGED
package/firewall-only.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// Do nothing inside Node's internal ESM-hooks thread (see internal-thread.js).
|
|
4
4
|
if (require('./internal-thread').inInternalThread()) return;
|
|
5
|
+
// SECURENOW_DISABLED=1 turns the SDK off for this process (test runs).
|
|
6
|
+
if (require('./sdk-disabled').sdkDisabled('firewall')) return;
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Standalone firewall preload — no OpenTelemetry, no tracing.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Route matching for firewall block, rate-limit and challenge rules.
|
|
4
|
+
//
|
|
5
|
+
// Many servers answer more than one spelling of a path: Express 5 routes case-insensitively and ignores
|
|
6
|
+
// a trailing slash, so POST /API/AUTH/LOGIN/ reaches the /api/auth/login handler. A rule matched only
|
|
7
|
+
// against the raw path let those aliases walk past a route-scoped block. Rules now also match the
|
|
8
|
+
// canonical form (percent-decoded, lower-case, duplicate slashes collapsed; exact rules also ignore a
|
|
9
|
+
// trailing slash). Every caller enforces (block / rate-limit / challenge), so matching more spellings
|
|
10
|
+
// is the safe direction.
|
|
11
|
+
|
|
12
|
+
function normalizePrefixPattern(pattern) {
|
|
13
|
+
const value = String(pattern || '').trim();
|
|
14
|
+
if (!value) return '';
|
|
15
|
+
return value.endsWith('*') ? value.slice(0, -1) : value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function canonicalPath(path, { stripTrailingSlash = false } = {}) {
|
|
19
|
+
let value = String(path || '/');
|
|
20
|
+
try { value = decodeURIComponent(value); } catch (_) { /* keep the raw spelling */ }
|
|
21
|
+
value = value.toLowerCase().replace(/\/{2,}/g, '/');
|
|
22
|
+
if (stripTrailingSlash && value.length > 1) value = value.replace(/\/+$/, '') || '/';
|
|
23
|
+
return value || '/';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function rulePathMatches(rule, path) {
|
|
27
|
+
const pattern = String((rule && rule.pathPattern) || '').trim();
|
|
28
|
+
if (!pattern) return true;
|
|
29
|
+
const mode = (rule && rule.pathMatchMode) || 'prefix';
|
|
30
|
+
if (mode === 'regex') {
|
|
31
|
+
try {
|
|
32
|
+
const re = new RegExp(pattern);
|
|
33
|
+
return re.test(path) || re.test(canonicalPath(path, { stripTrailingSlash: true }));
|
|
34
|
+
} catch (_) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (mode === 'exact') {
|
|
39
|
+
return path === pattern
|
|
40
|
+
|| canonicalPath(path, { stripTrailingSlash: true }) === canonicalPath(pattern, { stripTrailingSlash: true });
|
|
41
|
+
}
|
|
42
|
+
const prefix = normalizePrefixPattern(pattern);
|
|
43
|
+
return path.startsWith(prefix) || canonicalPath(path).startsWith(canonicalPath(prefix));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { canonicalPath, normalizePrefixPattern, rulePathMatches };
|
package/firewall.js
CHANGED
|
@@ -911,16 +911,8 @@ function ruleIpMatches(rule, ip) {
|
|
|
911
911
|
}
|
|
912
912
|
}
|
|
913
913
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
if (!pattern) return true;
|
|
917
|
-
const mode = rule.pathMatchMode || 'prefix';
|
|
918
|
-
if (mode === 'exact') return path === pattern;
|
|
919
|
-
if (mode === 'regex') {
|
|
920
|
-
try { return new RegExp(pattern).test(path); } catch (_) { return false; }
|
|
921
|
-
}
|
|
922
|
-
return path.startsWith(normalizePrefixPattern(pattern));
|
|
923
|
-
}
|
|
914
|
+
// Matches the raw path and its canonical aliases (case, trailing/duplicate slashes, percent-encoding).
|
|
915
|
+
const { rulePathMatches } = require('./firewall-paths');
|
|
924
916
|
|
|
925
917
|
function rateLimitKey(rule, ip) {
|
|
926
918
|
const id = rule.id || rule._id || `${rule.ip || '*'}:${rule.method || 'ALL'}:${rule.pathPattern || '*'}`;
|
package/log-format.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Body of an exported console log record, matching what the terminal shows.
|
|
4
|
+
// - A first string argument with printf-style placeholders (%s %d %i %f %j %o %O %c %%) is formatted
|
|
5
|
+
// like console.log does (util.format), so "%d users" becomes "3 users" instead of "%d users 3".
|
|
6
|
+
// - Errors keep their stack (JSON.stringify(new Error('x')) is "{}").
|
|
7
|
+
// - Other objects stay JSON, as before, so structured logs remain parseable.
|
|
8
|
+
const util = require('util');
|
|
9
|
+
|
|
10
|
+
const PLACEHOLDER = /%[sdifjoOc%]/;
|
|
11
|
+
|
|
12
|
+
function renderArg(arg) {
|
|
13
|
+
if (arg instanceof Error) return arg.stack || `${arg.name}: ${arg.message}`;
|
|
14
|
+
if (typeof arg === 'object' && arg !== null) {
|
|
15
|
+
try { return JSON.stringify(arg); } catch (_) { return util.inspect(arg, { depth: 4, breakLength: Infinity }); }
|
|
16
|
+
}
|
|
17
|
+
return String(arg);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatLogArgs(args) {
|
|
21
|
+
if (!Array.isArray(args) || args.length === 0) return '';
|
|
22
|
+
if (typeof args[0] === 'string' && PLACEHOLDER.test(args[0])) {
|
|
23
|
+
try { return util.format(...args); } catch (_) { /* fall through */ }
|
|
24
|
+
}
|
|
25
|
+
return args.map(renderArg).join(' ');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { formatLogArgs };
|
package/nextjs.js
CHANGED
|
@@ -221,6 +221,7 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
221
221
|
* @param {boolean} options.noUuid - Don't append UUID to service name
|
|
222
222
|
*/
|
|
223
223
|
function registerSecureNow(options = {}) {
|
|
224
|
+
if (require('./sdk-disabled').sdkDisabled('nextjs')) return;
|
|
224
225
|
// Only register once
|
|
225
226
|
if (isRegistered) {
|
|
226
227
|
console.log('[securenow] Already registered, skipping...');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "securenow",
|
|
3
|
-
"version": "8.12.
|
|
3
|
+
"version": "8.12.2",
|
|
4
4
|
"description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "register.js",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"securenow-mcp": "mcp/server.js"
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
|
-
"test": "node test-app-config.js && node test-resolve-ip.js && node test-otel-defaults.js && node test-telemetry.js && node test-legacy-semconv.js && node test-otp-redaction.js && node test-header-redaction.js && node test-internal-thread.js && node test-package-contents.js"
|
|
13
|
+
"test": "node test-app-config.js && node test-resolve-ip.js && node test-otel-defaults.js && node test-telemetry.js && node test-legacy-semconv.js && node test-otp-redaction.js && node test-header-redaction.js && node test-firewall-paths.js && node test-log-format.js && node test-sdk-disabled.js && node test-internal-thread.js && node test-package-contents.js"
|
|
14
14
|
},
|
|
15
15
|
"keywords": [
|
|
16
16
|
"opentelemetry",
|
|
@@ -162,6 +162,9 @@
|
|
|
162
162
|
"legacy-semconv.js",
|
|
163
163
|
"otp-redaction.js",
|
|
164
164
|
"header-redaction.js",
|
|
165
|
+
"firewall-paths.js",
|
|
166
|
+
"log-format.js",
|
|
167
|
+
"sdk-disabled.js",
|
|
165
168
|
"rate-limits.js",
|
|
166
169
|
"rate-limits.d.ts",
|
|
167
170
|
"firewall-only.js",
|
package/register.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
// Do nothing inside Node's internal ESM-hooks thread (see internal-thread.js).
|
|
10
10
|
if (require('./internal-thread').inInternalThread()) return;
|
|
11
|
+
// SECURENOW_DISABLED=1 turns the SDK off for this process (test runs).
|
|
12
|
+
if (require('./sdk-disabled').sdkDisabled('register')) return;
|
|
11
13
|
|
|
12
14
|
// 1. Auto-register the ESM loader hook so customers never need --import
|
|
13
15
|
(() => {
|
package/sdk-disabled.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Kill switch: SECURENOW_DISABLED=1 (or true / yes / on) turns the SDK off for one process. Tracing, log
|
|
4
|
+
// export, the firewall, track() events and the sessions guard's sync and session.seen all stay idle, and
|
|
5
|
+
// the guard just calls next(). Meant for unit and integration test runs, which otherwise report to the
|
|
6
|
+
// real app with the project's .securenow credentials.
|
|
7
|
+
let _announced = false;
|
|
8
|
+
|
|
9
|
+
function sdkDisabled(component) {
|
|
10
|
+
const on = /^(1|true|yes|on)$/i.test(String(process.env.SECURENOW_DISABLED || '').trim());
|
|
11
|
+
if (on && component && !_announced) {
|
|
12
|
+
_announced = true;
|
|
13
|
+
try { console.log(`[securenow] disabled by SECURENOW_DISABLED (${component} not started)`); } catch (_) { /* ignore */ }
|
|
14
|
+
}
|
|
15
|
+
return on;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { sdkDisabled };
|
package/sessions.js
CHANGED
|
@@ -130,6 +130,7 @@ function isRevoked(input) {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
function start(options = {}) {
|
|
133
|
+
if (require('./sdk-disabled').sdkDisabled()) return;
|
|
133
134
|
if (options.syncIntervalMs) _syncIntervalMs = options.syncIntervalMs;
|
|
134
135
|
if (_started) return;
|
|
135
136
|
_started = true;
|
|
@@ -277,6 +278,7 @@ function defaultOnRevoked(req, res) {
|
|
|
277
278
|
* @param {number} [options.syncIntervalMs]
|
|
278
279
|
*/
|
|
279
280
|
function guard(options = {}) {
|
|
281
|
+
if (require('./sdk-disabled').sdkDisabled('sessions guard')) return function securenowSessionGuardDisabled(req, res, next) { return next(); };
|
|
280
282
|
start(options);
|
|
281
283
|
if (options.captureWindowMs) _captureWindowMs = options.captureWindowMs;
|
|
282
284
|
const { getSessionId, getUserId } = options;
|
package/tracing.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// Do nothing inside Node's internal ESM-hooks thread (see internal-thread.js).
|
|
4
4
|
if (require('./internal-thread').inInternalThread()) return;
|
|
5
|
+
// SECURENOW_DISABLED=1 turns the SDK off for this process (test runs).
|
|
6
|
+
if (require('./sdk-disabled').sdkDisabled('tracing')) return;
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Preload with: node --require securenow/register app.js
|
|
@@ -665,7 +667,7 @@ if (loggingEnabled) {
|
|
|
665
667
|
_logger.emit({
|
|
666
668
|
severityNumber: sn,
|
|
667
669
|
severityText: st,
|
|
668
|
-
body:
|
|
670
|
+
body: require('./log-format').formatLogArgs(args),
|
|
669
671
|
attributes: { 'log.source': 'console', 'log.method': st.toLowerCase() },
|
|
670
672
|
...(spanCtx && { context: activeCtx }),
|
|
671
673
|
});
|