securenow 8.24.0 → 8.25.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/NPM_README.md +693 -305
- package/README.md +76 -28
- package/SKILL-API.md +648 -525
- package/SKILL-CLI.md +293 -663
- package/app-config.js +19 -1
- package/cidr.js +139 -20
- package/cli/init.js +77 -15
- package/cli/utils.js +26 -14
- package/cli.js +7 -5
- package/console-instrumentation.js +6 -0
- package/firewall-cloud.js +81 -32
- package/firewall-iptables.js +123 -62
- package/firewall-only.js +2 -0
- package/firewall-tcp.js +19 -5
- package/firewall.js +21 -1
- package/framework-routes.js +222 -0
- package/log-format.js +2 -0
- package/log-redaction.js +135 -0
- package/mcp/catalog.js +14 -9
- package/next-route.js +61 -0
- package/nextjs-middleware.js +8 -4
- package/nextjs-wrapper.js +8 -4
- package/nextjs.d.ts +35 -67
- package/nextjs.js +29 -6
- package/package.json +5 -2
- package/register.d.ts +27 -34
- package/request-capture.js +11 -6
- package/resolve-ip.js +23 -5
- package/sessions.d.ts +27 -0
- package/sessions.js +71 -10
- package/tracing.d.ts +40 -121
- package/tracing.js +12 -4
package/log-redaction.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Exported log lines are redacted before they leave the process (the terminal still shows them as
|
|
5
|
+
* written). Two passes:
|
|
6
|
+
* - Credential fields, by exact name (case and separators ignored): in objects passed to console.*
|
|
7
|
+
* (redactLogArgs, before formatting) and in JSON or key=value text inside the line. password,
|
|
8
|
+
* token, apiKey, authorization, cookie, OTP codes, card numbers... `code` only when it holds a
|
|
9
|
+
* 4-8 digit OTP, so error codes stay readable.
|
|
10
|
+
* - Secret shapes anywhere in the line, as for captured request bodies: JWTs, Bearer/Basic
|
|
11
|
+
* credentials, sk_/pk_/rk_/snk_ keys, card numbers (Luhn-checked), emails.
|
|
12
|
+
* The second pass runs as the first log processor, so console.*, getLogger().emit() and crash logs
|
|
13
|
+
* are all covered. Attributes set on getLogger().emit() are exported as set.
|
|
14
|
+
*
|
|
15
|
+
* config.logging.redact: false exports lines unchanged.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// Lines longer than this skip the shape scan (the credential and OTP assignment scans still run).
|
|
19
|
+
const MAX_LOG_SCAN_LENGTH = 262144;
|
|
20
|
+
|
|
21
|
+
const CREDENTIAL_KEYS = new Set([
|
|
22
|
+
'password', 'passwd', 'pwd', 'passphrase', 'secret', 'clientsecret', 'privatekey',
|
|
23
|
+
'token', 'accesstoken', 'refreshtoken', 'idtoken', 'sessiontoken', 'authtoken', 'bearer',
|
|
24
|
+
'apikey', 'xapikey', 'authorization', 'cookie', 'setcookie', 'sessionid',
|
|
25
|
+
'otp', 'otpcode', 'onetimecode', 'onetimepassword', 'passcode', 'verificationcode',
|
|
26
|
+
'confirmationcode', 'mfacode', 'totpcode', '2facode', 'smscode', 'emailcode', 'logincode',
|
|
27
|
+
'securitycode', 'recoverycode', 'backupcode',
|
|
28
|
+
'cardnumber', 'cvv', 'cvc', 'ccv', 'ssn', 'pin',
|
|
29
|
+
]);
|
|
30
|
+
const canonical = (key) => String(key).replace(/[^a-z0-9]/gi, '').toLowerCase();
|
|
31
|
+
const OTP_DIGITS = /^\d{4,8}$/;
|
|
32
|
+
|
|
33
|
+
function isCredentialKey(key, value) {
|
|
34
|
+
const name = canonical(key);
|
|
35
|
+
if (CREDENTIAL_KEYS.has(name)) return true;
|
|
36
|
+
return name === 'code' && OTP_DIGITS.test(String(value));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let _enabled;
|
|
40
|
+
function redactionEnabled() {
|
|
41
|
+
if (_enabled === undefined) {
|
|
42
|
+
try { _enabled = require('./app-config').boolConfig('logging.redact', true); } catch (_) { _enabled = true; }
|
|
43
|
+
}
|
|
44
|
+
return _enabled;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const MAX_DEPTH = 8;
|
|
48
|
+
const MAX_NODES = 2000;
|
|
49
|
+
|
|
50
|
+
function isPlainObject(v) {
|
|
51
|
+
if (!v || typeof v !== 'object') return false;
|
|
52
|
+
const proto = Object.getPrototypeOf(v);
|
|
53
|
+
return proto === Object.prototype || proto === null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Copies plain objects and arrays with credential values replaced. Class instances (Date, Map,
|
|
57
|
+
// Buffer, Error...) are kept as they are; cycles stay cycles; work is bounded.
|
|
58
|
+
function redactValue(value, state, depth) {
|
|
59
|
+
if (!(isPlainObject(value) || Array.isArray(value))) return value;
|
|
60
|
+
if (state.seen.has(value)) return state.seen.get(value);
|
|
61
|
+
if (depth > MAX_DEPTH || state.nodes > MAX_NODES) return value;
|
|
62
|
+
state.nodes += 1;
|
|
63
|
+
const out = Array.isArray(value) ? [] : {};
|
|
64
|
+
state.seen.set(value, out);
|
|
65
|
+
for (const key of Object.keys(value)) {
|
|
66
|
+
const v = value[key];
|
|
67
|
+
out[key] = !Array.isArray(value) && isCredentialKey(key, v) && v !== null && v !== undefined && v !== ''
|
|
68
|
+
? '[REDACTED]'
|
|
69
|
+
: redactValue(v, state, depth + 1);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** console.* arguments with credential fields redacted in objects (unchanged when redaction is off). */
|
|
75
|
+
function redactLogArgs(args) {
|
|
76
|
+
if (!Array.isArray(args) || !redactionEnabled()) return args;
|
|
77
|
+
let changed = false;
|
|
78
|
+
const state = { seen: new Map(), nodes: 0 };
|
|
79
|
+
const out = args.map((a) => {
|
|
80
|
+
const r = redactValue(a, state, 0);
|
|
81
|
+
if (r !== a) changed = true;
|
|
82
|
+
return r;
|
|
83
|
+
});
|
|
84
|
+
return changed ? out : args;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// "password": "x" (JSON) and password=x (key=value) in the text. Starts only at a quote or at the
|
|
88
|
+
// beginning of a word, so matching stays linear.
|
|
89
|
+
const JSON_FIELD = /"([A-Za-z0-9_-]{1,40})"(\s*:\s*)"((?:[^"\\]|\\.)*)"/g;
|
|
90
|
+
const KV_FIELD = /(^|[\s,;&?{(])([A-Za-z0-9_-]{1,40})=([^\s&,;)}]+)/g;
|
|
91
|
+
|
|
92
|
+
function redactCredentialText(text) {
|
|
93
|
+
if (text.indexOf('"') !== -1) {
|
|
94
|
+
text = text.replace(JSON_FIELD, (m, key, sep, value) => (isCredentialKey(key, value) && value ? `"${key}"${sep}"[REDACTED]"` : m));
|
|
95
|
+
}
|
|
96
|
+
if (text.indexOf('=') !== -1) {
|
|
97
|
+
text = text.replace(KV_FIELD, (m, lead, key, value) => (isCredentialKey(key, value) ? `${lead}${key}=[REDACTED]` : m));
|
|
98
|
+
}
|
|
99
|
+
return text;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function redactLogText(body) {
|
|
103
|
+
const { redactSensitiveValue } = require('./request-capture');
|
|
104
|
+
return redactSensitiveValue(redactCredentialText(body), MAX_LOG_SCAN_LENGTH);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function createRedactingLogProcessor() {
|
|
108
|
+
return {
|
|
109
|
+
onEmit(record) {
|
|
110
|
+
try {
|
|
111
|
+
const body = record && record.body;
|
|
112
|
+
if (typeof body !== 'string' || !body) return;
|
|
113
|
+
const out = redactLogText(body);
|
|
114
|
+
if (out !== body) record.setBody(out);
|
|
115
|
+
} catch (_) { /* never break logging */ }
|
|
116
|
+
},
|
|
117
|
+
forceFlush() { return Promise.resolve(); },
|
|
118
|
+
shutdown() { return Promise.resolve(); },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The processors a LoggerProvider gets: the redactor (unless config.logging.redact is false),
|
|
123
|
+
// then the exporting processor.
|
|
124
|
+
function logProcessors(appConfig, exportingProcessor) {
|
|
125
|
+
const redact = appConfig.boolConfig('logging.redact', true);
|
|
126
|
+
return redact ? [createRedactingLogProcessor(), exportingProcessor] : [exportingProcessor];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
createRedactingLogProcessor,
|
|
131
|
+
logProcessors,
|
|
132
|
+
redactLogArgs,
|
|
133
|
+
redactLogText,
|
|
134
|
+
MAX_LOG_SCAN_LENGTH,
|
|
135
|
+
};
|
package/mcp/catalog.js
CHANGED
|
@@ -46,21 +46,26 @@ Runbook:
|
|
|
46
46
|
- Confirm the runtime file has an app key/name/instance and a runtime API key after app selection. The app key only routes telemetry; the runtime API key is required to authenticate telemetry ingestion and firewall sync.
|
|
47
47
|
- Confirm .securenow/admin.json exists only when admin CLI/MCP auth is needed.
|
|
48
48
|
- Confirm .securenow/admin.json, .securenow/runtime.json, legacy .securenow/credentials.json, and any .securenow/credentials.*.json runtime files are ignored by git, without ignoring the entire .securenow/ directory.
|
|
49
|
-
6. Run npx securenow init.
|
|
50
|
-
7. Configure the least invasive framework-specific integration:
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
49
|
+
6. Run npx securenow init. It detects the framework, writes the Next.js instrumentation file and next.config changes, prints the start-script change for plain Node, gitignores the credential files, and warns about a PM2 ecosystem file that uses a bare -r. If it fails, upgrade to securenow@latest and retry. Do not silently ignore init failures.
|
|
50
|
+
7. Configure the least invasive framework-specific integration (help: npx securenow help init; full guide: node_modules/securenow/SKILL-API.md):
|
|
51
|
+
- Plain Node servers (Express, Fastify, NestJS, Koa, Hapi, Hono with @hono/node-server, Polka, Feathers, h3, raw node:http): preload securenow/register in the existing start command: node -r securenow/register <entry>. The same flag works for CommonJS, ESM and TypeScript (node -r securenow/register --import tsx src/index.ts). Alternatives: NODE_OPTIONS="-r securenow/register", npx securenow run <entry>, or require('securenow/register') as the first line of a CommonJS entry.
|
|
52
|
+
- PM2: node_args: ['-r', require.resolve('securenow/register')] in ecosystem.config.cjs (fork or cluster). Never a bare '-r securenow/register' in PM2: cluster workers resolve it from the daemon's folder and crash-loop.
|
|
53
|
+
- Docker: CMD ["node", "-r", "securenow/register", "<entry>"] with WORKDIR set to the app folder.
|
|
54
|
+
- Next.js 15 and 16: keep the instrumentation file npx securenow init writes (it imports securenow/nextjs and securenow/nextjs-auto-capture inside if (process.env.NEXT_RUNTIME === 'nodejs') with a try/catch) and its next.config changes: serverExternalPackages: ['securenow'] plus outputFileTracingIncludes: { '/*': ['./node_modules/securenow/**/*'] }. Next.js 14: experimental.instrumentationHook, experimental.serverComponentsExternalPackages and experimental.outputFileTracingIncludes instead. Works with next start and output: 'standalone' (put .securenow/credentials.json next to the standalone server.js). Preserve existing instrumentation, proxy.js and middleware.js code. The Edge runtime is not instrumented.
|
|
55
|
+
- Nuxt 3/4: modules: ['securenow/nuxt'] in nuxt.config, no securenow options block (settings live in .securenow/runtime.json). Alternative: node -r securenow/register .output/server/index.mjs.
|
|
56
|
+
- Vite: browser telemetry is not sent to SecureNow. Instrument the Node process that serves the app: node -r securenow/register-vite node_modules/vite/bin/vite.js preview, or -r securenow/register for a Vite SSR server. Say that the firewall only protects a server runtime.
|
|
57
|
+
- Bun 1.3: bun --preload securenow/register <entry> for apps served through node:http. Bun.serve, Deno, Cloudflare Workers and other edge runtimes are not supported.
|
|
58
|
+
- Firewall-only: node -r securenow/firewall-only <entry>, or npx securenow run --firewall-only <entry>. Do not add OTel, tracing or logging in this mode.
|
|
59
|
+
- App-level features, when the app has authentication: send auth events with track() from securenow/events (auth.login.failure, auth.login.success, auth.otp.verify.failure, auth.password_reset.requested, ...) at the real login/OTP/reset code paths, and enable session revocation with "sessions": { "guard": true, "capture": true } under config in .securenow/runtime.json (add the app's session cookie names to sessions.cookieNames if they are not the defaults). Ask before changing auth code.
|
|
56
60
|
8. Do not create or require a .env file for local development or production. The SDK reads defaults from .securenow/runtime.json, with legacy .securenow/credentials.json and generated runtime credential files still supported:
|
|
57
61
|
- config.logging.enabled: true
|
|
58
62
|
- config.capture.body: true
|
|
59
63
|
- config.capture.multipart: true
|
|
60
|
-
- config.firewall.enabled: true
|
|
61
64
|
- config.firewall.failMode: "open"
|
|
62
65
|
- config.capture.maxBodySize: 10240
|
|
63
|
-
|
|
66
|
+
- config.logging.redact: true (credential fields and secret shapes in exported log lines become [REDACTED])
|
|
67
|
+
Firewall enforcement is the per-app toggle in SecureNow (npx securenow firewall enable --app <appKey>), not a local setting.
|
|
68
|
+
For production, run npx securenow credentials runtime --env production, store the resulting JSON as a deployment secret file, and mount/copy it to <app-root>/.securenow/credentials.json or <app-root>/.securenow/credentials.production.json. Hosts that cannot mount a file (Vercel, Railway, Render, Heroku, Fly): npx securenow credentials runtime --env production --format env prints SECURENOW_CREDENTIALS=<base64>; store it as a secret environment variable. No other SECURENOW_* variable is read (SECURENOW_DISABLED=1 turns the SDK off, for tests). Vercel, AWS Lambda and other serverless platforms are not tested end to end: say so, and do not promise firewall blocking there.
|
|
64
69
|
Do not proceed with telemetry verification if the runtime API key is missing; run npx securenow app connect or npx securenow api-key create first. The v8 ingestion gateway rejects app-key-only telemetry.
|
|
65
70
|
Local credentials should use config.runtime.deploymentEnvironment="local". Production runtime credentials should use "production". The app key stays the same; traces, logs, firewall status, forensics, and CLI/MCP queries are scoped by environment.
|
|
66
71
|
9. Verify firewall and threshold:
|
package/next-route.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* http.route on the HTTP-layer span of a Next.js request.
|
|
5
|
+
*
|
|
6
|
+
* Next.js records the route on its own spans (next.route, often several levels below the request's HTTP
|
|
7
|
+
* span); detection and the dashboard read the HTTP span, which had none on Next.js 15 (16 sets http.route
|
|
8
|
+
* on its own span only). This span processor copies the first route Next.js reports in a trace onto that
|
|
9
|
+
* trace's HTTP server span, which is still open because Next.js finishes its spans before the response ends.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const HTTP_SCOPE = '@opentelemetry/instrumentation-http';
|
|
13
|
+
const SERVER = 1; // SpanKind.SERVER
|
|
14
|
+
const MAX_OPEN = 10000;
|
|
15
|
+
|
|
16
|
+
function scopeName(span) {
|
|
17
|
+
const scope = span && (span.instrumentationScope || span.instrumentationLibrary);
|
|
18
|
+
return scope && scope.name;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createNextRouteProcessor() {
|
|
22
|
+
const open = new Map(); // traceId -> { span, routed }
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
onStart(span) {
|
|
26
|
+
try {
|
|
27
|
+
if (span.kind !== SERVER || scopeName(span) !== HTTP_SCOPE) return;
|
|
28
|
+
const { traceId } = span.spanContext();
|
|
29
|
+
if (open.has(traceId)) return;
|
|
30
|
+
if (open.size >= MAX_OPEN) open.clear();
|
|
31
|
+
open.set(traceId, { span, spanId: span.spanContext().spanId, routed: false });
|
|
32
|
+
} catch (_) {}
|
|
33
|
+
},
|
|
34
|
+
onEnd(span) {
|
|
35
|
+
try {
|
|
36
|
+
const { traceId, spanId } = span.spanContext();
|
|
37
|
+
const entry = open.get(traceId);
|
|
38
|
+
if (!entry) return;
|
|
39
|
+
if (entry.spanId === spanId) {
|
|
40
|
+
open.delete(traceId);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const route = span.attributes && span.attributes['next.route'];
|
|
44
|
+
// Next.js also reports module paths here ("\hello\route" on Windows): only URL routes count.
|
|
45
|
+
if (!entry.routed && typeof route === 'string' && route.startsWith('/')) {
|
|
46
|
+
entry.span.setAttribute('http.route', route);
|
|
47
|
+
entry.routed = true;
|
|
48
|
+
}
|
|
49
|
+
} catch (_) {}
|
|
50
|
+
},
|
|
51
|
+
forceFlush() {
|
|
52
|
+
return Promise.resolve();
|
|
53
|
+
},
|
|
54
|
+
shutdown() {
|
|
55
|
+
open.clear();
|
|
56
|
+
return Promise.resolve();
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { createNextRouteProcessor };
|
package/nextjs-middleware.js
CHANGED
|
@@ -51,18 +51,21 @@ const DEFAULT_SENSITIVE_FIELDS = [
|
|
|
51
51
|
// and they only ever transform captured telemetry strings (read-only) — never
|
|
52
52
|
// the actual request/response stream. Compiled once at module load.
|
|
53
53
|
const VALUE_REDACTORS = [
|
|
54
|
+
// The JWT and email patterns only start where a run of their characters starts (the
|
|
55
|
+
// lookbehind), so a long run that holds no match is scanned once, not once per position:
|
|
56
|
+
// a crafted 10KB value costs microseconds, not 50ms+ of event loop.
|
|
54
57
|
// JWT: three base64url segments. Anchored to the eyJ header so it won't
|
|
55
58
|
// match arbitrary dotted tokens.
|
|
56
|
-
{ name: 'jwt', re: /eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}/g },
|
|
59
|
+
{ name: 'jwt', hint: 'eyJ', re: /(?<![A-Za-z0-9_-])eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}/g },
|
|
57
60
|
// Bearer/Basic auth header value embedded in a body string.
|
|
58
61
|
{ name: 'bearer', re: /\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi },
|
|
59
62
|
// Stripe-style / SecureNow live+test API keys.
|
|
60
63
|
{ name: 'apikey', re: /\b(?:sk|pk|rk|snk)_(?:live|test)_[A-Za-z0-9]{8,}/g },
|
|
61
|
-
// Email addresses (bounded local/domain parts).
|
|
62
|
-
{ name: 'email', re:
|
|
64
|
+
// Email addresses (bounded local/domain parts: 64 and 253 characters at most).
|
|
65
|
+
{ name: 'email', hint: '@', re: /(?<![\w.%+-])[\w.%+-]{1,64}@[A-Za-z0-9.-]{1,253}\.[A-Za-z]{2,24}\b/g },
|
|
63
66
|
// Credit-card-like: 13–19 digit runs that pass a Luhn check, allowing
|
|
64
67
|
// space/dash grouping. Luhn-gated to avoid clobbering ordinary long numbers.
|
|
65
|
-
{ name: 'card', re: /\b(?:\d[ -]?){
|
|
68
|
+
{ name: 'card', re: /\b(?:\d[ -]?){12,18}\d\b/g, luhn: true },
|
|
66
69
|
];
|
|
67
70
|
|
|
68
71
|
// Skip the value-shape scan on very large strings to bound per-body cost.
|
|
@@ -93,6 +96,7 @@ function redactSensitiveValue(value) {
|
|
|
93
96
|
if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound shape scans
|
|
94
97
|
let out = value;
|
|
95
98
|
for (const r of VALUE_REDACTORS) {
|
|
99
|
+
if (r.hint && !out.includes(r.hint)) continue;
|
|
96
100
|
r.re.lastIndex = 0;
|
|
97
101
|
if (r.luhn) {
|
|
98
102
|
out = out.replace(r.re, (m) => {
|
package/nextjs-wrapper.js
CHANGED
|
@@ -51,18 +51,21 @@ const DEFAULT_SENSITIVE_FIELDS = [
|
|
|
51
51
|
// and they only ever transform captured telemetry strings (read-only) — never
|
|
52
52
|
// the actual request/response stream. Compiled once at module load.
|
|
53
53
|
const VALUE_REDACTORS = [
|
|
54
|
+
// The JWT and email patterns only start where a run of their characters starts (the
|
|
55
|
+
// lookbehind), so a long run that holds no match is scanned once, not once per position:
|
|
56
|
+
// a crafted 10KB value costs microseconds, not 50ms+ of event loop.
|
|
54
57
|
// JWT: three base64url segments. Anchored to the eyJ header so it won't
|
|
55
58
|
// match arbitrary dotted tokens.
|
|
56
|
-
{ name: 'jwt', re: /eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}/g },
|
|
59
|
+
{ name: 'jwt', hint: 'eyJ', re: /(?<![A-Za-z0-9_-])eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}/g },
|
|
57
60
|
// Bearer/Basic auth header value embedded in a body string.
|
|
58
61
|
{ name: 'bearer', re: /\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi },
|
|
59
62
|
// Stripe-style / SecureNow live+test API keys.
|
|
60
63
|
{ name: 'apikey', re: /\b(?:sk|pk|rk|snk)_(?:live|test)_[A-Za-z0-9]{8,}/g },
|
|
61
|
-
// Email addresses (bounded local/domain parts).
|
|
62
|
-
{ name: 'email', re:
|
|
64
|
+
// Email addresses (bounded local/domain parts: 64 and 253 characters at most).
|
|
65
|
+
{ name: 'email', hint: '@', re: /(?<![\w.%+-])[\w.%+-]{1,64}@[A-Za-z0-9.-]{1,253}\.[A-Za-z]{2,24}\b/g },
|
|
63
66
|
// Credit-card-like: 13–19 digit runs that pass a Luhn check, allowing
|
|
64
67
|
// space/dash grouping. Luhn-gated to avoid clobbering ordinary long numbers.
|
|
65
|
-
{ name: 'card', re: /\b(?:\d[ -]?){
|
|
68
|
+
{ name: 'card', re: /\b(?:\d[ -]?){12,18}\d\b/g, luhn: true },
|
|
66
69
|
];
|
|
67
70
|
|
|
68
71
|
// Skip the value-shape scan on very large strings to bound per-body cost.
|
|
@@ -93,6 +96,7 @@ function redactSensitiveValue(value) {
|
|
|
93
96
|
if (value.length > MAX_VALUE_SCAN_LENGTH) return value; // bound shape scans
|
|
94
97
|
let out = value;
|
|
95
98
|
for (const r of VALUE_REDACTORS) {
|
|
99
|
+
if (r.hint && !out.includes(r.hint)) continue;
|
|
96
100
|
r.re.lastIndex = 0;
|
|
97
101
|
if (r.luhn) {
|
|
98
102
|
out = out.replace(r.re, (m) => {
|
package/nextjs.d.ts
CHANGED
|
@@ -1,102 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SecureNow Next.js Integration TypeScript Declarations
|
|
3
|
+
*
|
|
4
|
+
* Settings live in .securenow/runtime.json (or SECURENOW_CREDENTIALS on hosts without file
|
|
5
|
+
* mounts); the options below only override them.
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
8
|
export interface RegisterOptions {
|
|
6
9
|
/**
|
|
7
|
-
* Service name for OpenTelemetry traces
|
|
8
|
-
*
|
|
10
|
+
* Service name for OpenTelemetry traces.
|
|
11
|
+
* Leave unset: SecureNow routes telemetry by the app key, and another name hides the data
|
|
12
|
+
* from the app's dashboard. Only for a self-hosted collector.
|
|
13
|
+
* @default the app key from .securenow/runtime.json
|
|
9
14
|
*/
|
|
10
15
|
serviceName?: string;
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
|
-
* OTLP endpoint for
|
|
18
|
+
* Advanced OTLP endpoint override, for a self-hosted collector. Leave unset for SecureNow.
|
|
14
19
|
* @default https://ingest.securenow.ai
|
|
15
|
-
*
|
|
16
|
-
* Advanced OTLP endpoint override. Normal SecureNow apps should leave this
|
|
17
|
-
* unset so the ingest gateway can route by app.key to the dashboard-selected
|
|
18
|
-
* instance.
|
|
19
20
|
*/
|
|
20
21
|
endpoint?: string;
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Deployment environment sent as OpenTelemetry deployment.environment.
|
|
24
|
-
* @default .
|
|
25
|
+
* @default config.runtime.deploymentEnvironment from .securenow/runtime.json
|
|
25
26
|
*/
|
|
26
27
|
environment?: string;
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
|
-
* Don't append UUID to service name
|
|
30
|
-
* @default
|
|
30
|
+
* Don't append a UUID to the service name.
|
|
31
|
+
* @default true when an app key is configured
|
|
31
32
|
*/
|
|
32
33
|
noUuid?: boolean;
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
*
|
|
36
|
-
* @default
|
|
35
|
+
/**
|
|
36
|
+
* Capture request bodies (redacted, size-capped, multipart as metadata only).
|
|
37
|
+
* @default config.capture.body from .securenow/runtime.json (true)
|
|
37
38
|
*/
|
|
38
39
|
captureBody?: boolean;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
|
-
* Register SecureNow
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* @example
|
|
47
|
-
* ```typescript
|
|
48
|
-
* // instrumentation.ts
|
|
49
|
-
* import { createRequire } from 'node:module';
|
|
50
|
-
*
|
|
51
|
-
* const require = createRequire(import.meta.url);
|
|
43
|
+
* Register SecureNow (tracing, logs, body capture, firewall, sessions) for the Next.js Node.js runtime.
|
|
44
|
+
* `npx securenow init` writes this file for you.
|
|
45
|
+
*
|
|
46
|
+
* @param options - Optional overrides
|
|
52
47
|
*
|
|
53
|
-
* export async function register() {
|
|
54
|
-
* if (process.env.NEXT_RUNTIME !== 'nodejs') return;
|
|
55
|
-
* const { registerSecureNow } = require('securenow/nextjs');
|
|
56
|
-
* registerSecureNow({ captureBody: true });
|
|
57
|
-
* require('securenow/nextjs-auto-capture');
|
|
58
|
-
* }
|
|
59
|
-
* ```
|
|
60
|
-
*
|
|
61
48
|
* @example
|
|
62
49
|
* ```typescript
|
|
63
|
-
* //
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
50
|
+
* // instrumentation.ts (what `npx securenow init` writes)
|
|
51
|
+
* export async function register() {
|
|
52
|
+
* if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
53
|
+
* try {
|
|
54
|
+
* const securenowNext = await import('securenow/nextjs');
|
|
55
|
+
* const registerSecureNow = securenowNext.registerSecureNow || securenowNext.default?.registerSecureNow;
|
|
56
|
+
* registerSecureNow({ captureBody: true });
|
|
57
|
+
* await import('securenow/nextjs-auto-capture');
|
|
58
|
+
* } catch (error) {
|
|
59
|
+
* console.error('[securenow] SecureNow did not start; the app is running without it:', error);
|
|
60
|
+
* }
|
|
61
|
+
* }
|
|
72
62
|
* }
|
|
73
63
|
* ```
|
|
64
|
+
*
|
|
65
|
+
* next.config: `serverExternalPackages: ['securenow']` and
|
|
66
|
+
* `outputFileTracingIncludes: { '/*': ['./node_modules/securenow/**\/*'] }` (Next.js 15+);
|
|
67
|
+
* Next.js 14 uses `experimental.instrumentationHook`, `experimental.serverComponentsExternalPackages`
|
|
68
|
+
* and `experimental.outputFileTracingIncludes`.
|
|
74
69
|
*/
|
|
75
70
|
export function registerSecureNow(options?: RegisterOptions): void;
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Default sensitive fields that are automatically redacted from traces
|
|
79
|
-
*/
|
|
80
|
-
export const DEFAULT_SENSITIVE_FIELDS: readonly string[];
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Redact sensitive fields from an object
|
|
84
|
-
* @param obj - Object to redact
|
|
85
|
-
* @param sensitiveFields - Array of field names to redact (case-insensitive substring match)
|
|
86
|
-
* @returns Redacted copy of the object
|
|
87
|
-
*/
|
|
88
|
-
export function redactSensitiveData<T = any>(
|
|
89
|
-
obj: T,
|
|
90
|
-
sensitiveFields?: string[]
|
|
91
|
-
): T;
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Redact sensitive data from GraphQL query strings
|
|
95
|
-
* @param query - GraphQL query string
|
|
96
|
-
* @param sensitiveFields - Array of field names to redact
|
|
97
|
-
* @returns Redacted query string
|
|
98
|
-
*/
|
|
99
|
-
export function redactGraphQLQuery(
|
|
100
|
-
query: string,
|
|
101
|
-
sensitiveFields?: string[]
|
|
102
|
-
): string;
|
package/nextjs.js
CHANGED
|
@@ -140,7 +140,8 @@ function registerSecureNow(options = {}) {
|
|
|
140
140
|
const otelLogLevel = String(appConfig.configValue('otel.logLevel', '') || '').toLowerCase();
|
|
141
141
|
const isDevelopmentRuntime = process.env.NODE_ENV === 'development';
|
|
142
142
|
|
|
143
|
-
console.log('[securenow] Next.js App -> service.name=%s', serviceName
|
|
143
|
+
console.log('[securenow] Next.js App -> service.name=%s apiKey=%s credentials=%s', serviceName,
|
|
144
|
+
appConfig.resolveApiKey() ? 'set' : 'missing', appConfig.describeCredentialsSource());
|
|
144
145
|
console.log('[securenow] Environment: %s', deploymentEnvironment);
|
|
145
146
|
|
|
146
147
|
// -------- Body Capture Configuration --------
|
|
@@ -438,10 +439,16 @@ function registerSecureNow(options = {}) {
|
|
|
438
439
|
headers
|
|
439
440
|
}));
|
|
440
441
|
|
|
442
|
+
// The batch exporter NodeSDK would build from traceExporter, plus the processor that puts Next.js's
|
|
443
|
+
// route on the request's HTTP span (see next-route.js).
|
|
444
|
+
const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-base');
|
|
441
445
|
const sdk = new NodeSDK({
|
|
442
446
|
...nodeSdkTelemetryOptions,
|
|
443
447
|
serviceName: serviceName,
|
|
444
|
-
|
|
448
|
+
spanProcessors: [
|
|
449
|
+
new BatchSpanProcessor(traceExporter),
|
|
450
|
+
require('./next-route').createNextRouteProcessor(),
|
|
451
|
+
],
|
|
445
452
|
instrumentations: [httpInstrumentation],
|
|
446
453
|
resource: createResource({
|
|
447
454
|
[SemanticResourceAttributes.SERVICE_NAME]: serviceName,
|
|
@@ -472,16 +479,21 @@ function registerSecureNow(options = {}) {
|
|
|
472
479
|
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: deploymentEnvironment,
|
|
473
480
|
}),
|
|
474
481
|
// sdk-logs >= 0.220 takes a single options object ({ exporter }).
|
|
475
|
-
|
|
482
|
+
// Secret shapes in log lines are redacted before export (log-redaction.js).
|
|
483
|
+
processors: require('./log-redaction').logProcessors(appConfig, new BatchLogRecordProcessor({ exporter: logExporter })),
|
|
476
484
|
});
|
|
477
485
|
_crashLoggerProvider = loggerProvider;
|
|
478
486
|
|
|
479
|
-
// Patch console to forward logs as OTLP log records
|
|
487
|
+
// Patch console to forward logs as OTLP log records, formatted like the Node preload's
|
|
488
|
+
// (printf placeholders, JSON objects, errors with their stack).
|
|
480
489
|
const logger = loggerProvider.getLogger('console', '1.0.0');
|
|
481
|
-
const SeverityNumber = { INFO: 9, WARN: 13, ERROR: 17 };
|
|
490
|
+
const SeverityNumber = { DEBUG: 5, INFO: 9, WARN: 13, ERROR: 17 };
|
|
482
491
|
const origLog = console.log;
|
|
492
|
+
const origInfo = console.info;
|
|
483
493
|
const origWarn = console.warn;
|
|
484
494
|
const origError = console.error;
|
|
495
|
+
const origDebug = console.debug;
|
|
496
|
+
const { formatLogArgs } = require('./log-format');
|
|
485
497
|
|
|
486
498
|
const { context: otelContext, trace: otelTrace } = require('@opentelemetry/api');
|
|
487
499
|
function _emitLog(sn, st, args) {
|
|
@@ -491,7 +503,8 @@ function registerSecureNow(options = {}) {
|
|
|
491
503
|
logger.emit({
|
|
492
504
|
severityNumber: sn,
|
|
493
505
|
severityText: st,
|
|
494
|
-
body: args
|
|
506
|
+
body: formatLogArgs(args),
|
|
507
|
+
attributes: { 'log.source': 'console', 'log.method': st.toLowerCase() },
|
|
495
508
|
...(spanCtx && { context: activeCtx }),
|
|
496
509
|
});
|
|
497
510
|
} catch (_) {}
|
|
@@ -500,6 +513,10 @@ function registerSecureNow(options = {}) {
|
|
|
500
513
|
origLog.apply(console, args);
|
|
501
514
|
_emitLog(SeverityNumber.INFO, 'INFO', args);
|
|
502
515
|
};
|
|
516
|
+
console.info = (...args) => {
|
|
517
|
+
origInfo.apply(console, args);
|
|
518
|
+
_emitLog(SeverityNumber.INFO, 'INFO', args);
|
|
519
|
+
};
|
|
503
520
|
console.warn = (...args) => {
|
|
504
521
|
origWarn.apply(console, args);
|
|
505
522
|
_emitLog(SeverityNumber.WARN, 'WARN', args);
|
|
@@ -508,6 +525,10 @@ function registerSecureNow(options = {}) {
|
|
|
508
525
|
origError.apply(console, args);
|
|
509
526
|
_emitLog(SeverityNumber.ERROR, 'ERROR', args);
|
|
510
527
|
};
|
|
528
|
+
console.debug = (...args) => {
|
|
529
|
+
origDebug.apply(console, args);
|
|
530
|
+
_emitLog(SeverityNumber.DEBUG, 'DEBUG', args);
|
|
531
|
+
};
|
|
511
532
|
|
|
512
533
|
console.log('[securenow] Logging: ENABLED -> %s', logsUrl);
|
|
513
534
|
|
|
@@ -627,6 +648,8 @@ function registerSecureNow(options = {}) {
|
|
|
627
648
|
aws: firewallOptions.aws,
|
|
628
649
|
gcp: firewallOptions.gcp,
|
|
629
650
|
});
|
|
651
|
+
// Zero-code session revocation / session-theft capture (config.sessions.guard / .capture).
|
|
652
|
+
requireRuntimeModule('./sessions').installHttpGuard(appConfig.resolveSessionsOptions());
|
|
630
653
|
} catch (e) {
|
|
631
654
|
console.warn('[securenow] Firewall init failed:', e.message);
|
|
632
655
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "securenow",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.25.0",
|
|
4
4
|
"mcpName": "ai.securenow/securenow",
|
|
5
5
|
"description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"securenow-mcp": "mcp/server.js"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
|
-
"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-firewall-guards.js && node test-rate-limits.js && node test-rate-limit-redis.js && node test-log-format.js && node test-crash-capture.js && node test-crash-flush.js && node test-request-capture.js && node test-fastify-hooks.js && node test-pm2-credentials.js && node test-env-credentials.js && node test-framework-wiring.js && node test-sdk-disabled.js && node test-internal-thread.js && node test-esm-detect.js && node test-package-contents.js"
|
|
14
|
+
"test": "node test-app-config.js && node test-resolve-ip.js && node test-cidr.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-firewall-guards.js && node test-rate-limits.js && node test-rate-limit-redis.js && node test-log-format.js && node test-log-redaction.js && node test-crash-capture.js && node test-crash-flush.js && node test-request-capture.js && node test-fastify-hooks.js && node test-pm2-credentials.js && node test-env-credentials.js && node test-framework-wiring.js && node test-framework-routes.js && node test-sessions-everywhere.js && node test-local-socket.js && node test-firewall-cloud.js && node test-firewall-layers.js && node test-sdk-disabled.js && node test-internal-thread.js && node test-esm-detect.js && node test-package-contents.js"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"opentelemetry",
|
|
@@ -167,10 +167,13 @@
|
|
|
167
167
|
"firewall-paths.js",
|
|
168
168
|
"rate-limit-store.js",
|
|
169
169
|
"log-format.js",
|
|
170
|
+
"log-redaction.js",
|
|
170
171
|
"crash-capture.js",
|
|
171
172
|
"request-capture.js",
|
|
172
173
|
"pm2-env.js",
|
|
173
174
|
"fastify-hooks.js",
|
|
175
|
+
"framework-routes.js",
|
|
176
|
+
"next-route.js",
|
|
174
177
|
"sdk-disabled.js",
|
|
175
178
|
"rate-limits.js",
|
|
176
179
|
"rate-limits.d.ts",
|