securenow 8.23.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 +730 -333
- package/README.md +91 -28
- package/SKILL-API.md +657 -530
- package/SKILL-CLI.md +293 -663
- package/app-config.js +90 -15
- package/cidr.js +139 -20
- package/cli/credentials.js +11 -0
- package/cli/init.js +121 -17
- package/cli/utils.js +26 -14
- package/cli.js +9 -6
- package/console-instrumentation.js +6 -0
- package/crash-capture.js +13 -1
- package/esm-detect.js +5 -1
- package/fastify-hooks.js +67 -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-auto-capture.d.ts +5 -2
- package/nextjs-auto-capture.js +77 -220
- package/nextjs-middleware.js +8 -4
- package/nextjs-wrapper.js +8 -4
- package/nextjs.d.ts +35 -67
- package/nextjs.js +75 -182
- package/nuxt-server-plugin.mjs +30 -478
- package/nuxt.mjs +15 -14
- package/package.json +8 -2
- package/pm2-env.js +47 -0
- package/register-vite.js +6 -2
- package/register.d.ts +27 -34
- package/request-capture.js +492 -0
- 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 +39 -447
package/sessions.d.ts
CHANGED
|
@@ -30,6 +30,33 @@ export function guard(options?: GuardOptions): (req: any, res: any, next: any) =
|
|
|
30
30
|
/** Express-style middleware that ONLY emits session.seen (detection feed, no enforcement). */
|
|
31
31
|
export function capture(options?: GuardOptions): (req: any, res: any, next: any) => void;
|
|
32
32
|
|
|
33
|
+
export interface CheckOptions {
|
|
34
|
+
/** Extract the session id. Defaults to the hashed Bearer token or session cookie. */
|
|
35
|
+
getSessionId?: (req: any) => string | null | undefined;
|
|
36
|
+
/** Extract the user id. Defaults to the JWT `sub` claim. */
|
|
37
|
+
getUserId?: (req: any) => string | null | undefined;
|
|
38
|
+
/** Client IP for session.seen when `req` has no socket (a Web Request). */
|
|
39
|
+
ip?: string;
|
|
40
|
+
/** Emit session.seen for detection (default true). */
|
|
41
|
+
capture?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* For code that cannot use guard() (Fastify hooks, Koa/Hono middleware, Next.js route handlers, h3):
|
|
46
|
+
* is this request's session or user revoked? `req` is a Node request or a Web `Request`. Fail-open. (8.25.0+)
|
|
47
|
+
*/
|
|
48
|
+
export function check(req: IncomingMessage | Request | { headers: any }, options?: CheckOptions): {
|
|
49
|
+
revoked: boolean;
|
|
50
|
+
sessionId: string | null;
|
|
51
|
+
userId: string | null;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Zero-code guard/capture for every framework, installed by the SDK from config.sessions.guard /
|
|
56
|
+
* config.sessions.capture in .securenow/runtime.json. Returns true when it installed the hook. (8.25.0+)
|
|
57
|
+
*/
|
|
58
|
+
export function installHttpGuard(options?: { guard?: boolean; capture?: boolean }): boolean;
|
|
59
|
+
|
|
33
60
|
/** Resolve the client IP from a request (X-Forwarded-For / CF / socket). */
|
|
34
61
|
export function resolveClientIp(req: any): string | null;
|
|
35
62
|
|
package/sessions.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
// half of account-takeover response).
|
|
5
5
|
//
|
|
6
6
|
// const securenow = require('securenow/sessions');
|
|
7
|
-
// app.use(securenow.guard()); // auto-detects NextAuth/connect.sid/JWT sessions
|
|
8
|
-
// // ...
|
|
9
|
-
// if (securenow.
|
|
7
|
+
// app.use(securenow.guard()); // Express-style; auto-detects NextAuth/connect.sid/JWT sessions
|
|
8
|
+
// // ...any other framework (Fastify, Koa, Hono, Next.js route handlers, h3):
|
|
9
|
+
// if (securenow.check(req).revoked) return /* a 401 */;
|
|
10
|
+
// // ...or no code at all: "sessions": { "guard": true, "capture": true } in .securenow/runtime.json
|
|
10
11
|
//
|
|
11
12
|
// The SDK pulls active revocations from SecureNow (outbound, ETag-cached) and
|
|
12
13
|
// checks each request locally — no inbound webhook, works behind NAT, portable
|
|
@@ -150,6 +151,21 @@ const SESSION_COOKIES = [
|
|
|
150
151
|
'sid',
|
|
151
152
|
];
|
|
152
153
|
|
|
154
|
+
// Session cookies: config.sessions.cookieNames first (the app's own), then the defaults.
|
|
155
|
+
function sessionCookieNames() {
|
|
156
|
+
let extra = [];
|
|
157
|
+
try { extra = appConfig.listConfig('sessions.cookieNames'); } catch {}
|
|
158
|
+
return extra.length ? [...extra, ...SESSION_COOKIES] : SESSION_COOKIES;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// A header from a Node request (plain object) or a Web Request (Headers).
|
|
162
|
+
function headerOf(req, name) {
|
|
163
|
+
const headers = req && req.headers;
|
|
164
|
+
if (!headers) return '';
|
|
165
|
+
if (typeof headers.get === 'function') return headers.get(name) || '';
|
|
166
|
+
return headers[name] || '';
|
|
167
|
+
}
|
|
168
|
+
|
|
153
169
|
function parseCookies(header) {
|
|
154
170
|
const out = {};
|
|
155
171
|
if (!header) return out;
|
|
@@ -165,8 +181,7 @@ function parseCookies(header) {
|
|
|
165
181
|
function defaultExtract(req) {
|
|
166
182
|
let sessionId = null;
|
|
167
183
|
let userId = null;
|
|
168
|
-
const
|
|
169
|
-
const auth = headers.authorization || headers.Authorization;
|
|
184
|
+
const auth = headerOf(req, 'authorization') || headerOf(req, 'Authorization');
|
|
170
185
|
if (auth && /^Bearer\s+/i.test(auth)) {
|
|
171
186
|
const token = auth.replace(/^Bearer\s+/i, '').trim();
|
|
172
187
|
if (token) {
|
|
@@ -178,9 +193,10 @@ function defaultExtract(req) {
|
|
|
178
193
|
} catch {}
|
|
179
194
|
}
|
|
180
195
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
196
|
+
const cookieHeader = headerOf(req, 'cookie');
|
|
197
|
+
if (!sessionId && cookieHeader) {
|
|
198
|
+
const cookies = parseCookies(cookieHeader);
|
|
199
|
+
for (const name of sessionCookieNames()) {
|
|
184
200
|
if (cookies[name]) {
|
|
185
201
|
sessionId = sha256(cookies[name]);
|
|
186
202
|
break;
|
|
@@ -255,7 +271,7 @@ function capture(options = {}) {
|
|
|
255
271
|
|
|
256
272
|
function defaultOnRevoked(req, res) {
|
|
257
273
|
try {
|
|
258
|
-
const clears =
|
|
274
|
+
const clears = sessionCookieNames().map((n) => `${n}=; Path=/; Max-Age=0; HttpOnly`);
|
|
259
275
|
if (typeof res.setHeader === 'function') res.setHeader('Set-Cookie', clears);
|
|
260
276
|
} catch {}
|
|
261
277
|
if (typeof res.status === 'function' && typeof res.json === 'function') {
|
|
@@ -298,4 +314,49 @@ function guard(options = {}) {
|
|
|
298
314
|
};
|
|
299
315
|
}
|
|
300
316
|
|
|
301
|
-
|
|
317
|
+
/**
|
|
318
|
+
* For code that cannot use guard() (Fastify hooks, Koa or Hono middleware, Next.js route handlers, h3):
|
|
319
|
+
* is this request's session or user revoked? `req` is a Node request or a Web Request. Feeds
|
|
320
|
+
* session-theft detection (session.seen) unless options.capture is false. Fail-open.
|
|
321
|
+
*
|
|
322
|
+
* @param {object} req
|
|
323
|
+
* @param {{ getSessionId?: (req)=>string, getUserId?: (req)=>string, ip?: string, capture?: boolean }} [options]
|
|
324
|
+
* @returns {{ revoked: boolean, sessionId: string|null, userId: string|null }}
|
|
325
|
+
*/
|
|
326
|
+
function check(req, options = {}) {
|
|
327
|
+
try {
|
|
328
|
+
if (require('./sdk-disabled').sdkDisabled('sessions check')) return { revoked: false, sessionId: null, userId: null };
|
|
329
|
+
start(options);
|
|
330
|
+
const { sessionId, userId } = extractIdentity(req, options.getSessionId, options.getUserId);
|
|
331
|
+
if (options.capture !== false) emitSessionSeen(sessionId, userId, options.ip || resolveClientIp(req));
|
|
332
|
+
return { revoked: isRevoked({ sessionId, userId }), sessionId: sessionId || null, userId: userId || null };
|
|
333
|
+
} catch {
|
|
334
|
+
return { revoked: false, sessionId: null, userId: null };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Zero code, every framework (config.sessions.guard / config.sessions.capture): the firewall's
|
|
339
|
+
// request hook runs this on each incoming request before the app sees it. guard answers a revoked
|
|
340
|
+
// session or user with a 401 and clears the session cookies; capture feeds session-theft detection.
|
|
341
|
+
let _httpGuardInstalled = false;
|
|
342
|
+
function installHttpGuard(options = {}) {
|
|
343
|
+
const enforce = !!options.guard;
|
|
344
|
+
const capture = !!options.capture;
|
|
345
|
+
if ((!enforce && !capture) || _httpGuardInstalled) return false;
|
|
346
|
+
if (require('./sdk-disabled').sdkDisabled('sessions')) return false;
|
|
347
|
+
_httpGuardInstalled = true;
|
|
348
|
+
start(options);
|
|
349
|
+
require('./firewall').addRequestHook(function securenowSessionHook(req, res) {
|
|
350
|
+
const { sessionId, userId } = defaultExtract(req);
|
|
351
|
+
if (!sessionId && !userId) return false;
|
|
352
|
+
if (capture) emitSessionSeen(sessionId, userId, resolveClientIp(req));
|
|
353
|
+
if (enforce && isRevoked({ sessionId, userId })) {
|
|
354
|
+
defaultOnRevoked(req, res);
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
return false;
|
|
358
|
+
});
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
module.exports = { start, syncOnce, isRevoked, guard, capture, check, installHttpGuard, resolveClientIp, sha256 };
|
package/tracing.d.ts
CHANGED
|
@@ -1,74 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SecureNow Tracing Module TypeScript Declarations
|
|
3
|
-
*
|
|
4
|
-
* Core tracing
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Default sensitive fields that are automatically redacted from request bodies
|
|
10
|
-
*/
|
|
11
|
-
export const DEFAULT_SENSITIVE_FIELDS: readonly string[];
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Redact sensitive fields from an object (recursively)
|
|
15
|
-
*
|
|
16
|
-
* @param obj - Object to redact (can be nested)
|
|
17
|
-
* @param sensitiveFields - Array of field names to redact (case-insensitive substring match)
|
|
18
|
-
* @returns Redacted copy of the object
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* ```typescript
|
|
22
|
-
* import { redactSensitiveData } from 'securenow/tracing';
|
|
23
|
-
*
|
|
24
|
-
* const data = {
|
|
25
|
-
* email: 'user@example.com',
|
|
26
|
-
* password: 'secret123',
|
|
27
|
-
* nested: {
|
|
28
|
-
* api_key: 'sk_live_abc123'
|
|
29
|
-
* }
|
|
30
|
-
* };
|
|
31
|
-
*
|
|
32
|
-
* const redacted = redactSensitiveData(data);
|
|
33
|
-
* // Result: {
|
|
34
|
-
* // email: 'user@example.com',
|
|
35
|
-
* // password: '[REDACTED]',
|
|
36
|
-
* // nested: { api_key: '[REDACTED]' }
|
|
37
|
-
* // }
|
|
38
|
-
* ```
|
|
39
|
-
*/
|
|
40
|
-
export function redactSensitiveData<T = any>(
|
|
41
|
-
obj: T,
|
|
42
|
-
sensitiveFields?: string[]
|
|
43
|
-
): T;
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Redact sensitive data from GraphQL query strings
|
|
47
|
-
*
|
|
48
|
-
* @param query - GraphQL query string
|
|
49
|
-
* @param sensitiveFields - Array of field names to redact
|
|
50
|
-
* @returns Redacted query string
|
|
51
|
-
*
|
|
52
|
-
* @example
|
|
53
|
-
* ```typescript
|
|
54
|
-
* import { redactGraphQLQuery } from 'securenow/tracing';
|
|
55
|
-
*
|
|
56
|
-
* const query = `
|
|
57
|
-
* mutation {
|
|
58
|
-
* login(email: "user@example.com", password: "secret123") {
|
|
59
|
-
* token
|
|
60
|
-
* }
|
|
61
|
-
* }
|
|
62
|
-
* `;
|
|
63
|
-
*
|
|
64
|
-
* const redacted = redactGraphQLQuery(query);
|
|
65
|
-
* // Result: mutation { login(email: "user@example.com", password: "[REDACTED]") { token } }
|
|
66
|
-
* ```
|
|
3
|
+
*
|
|
4
|
+
* Core tracing for Node.js applications. It starts when loaded, usually through
|
|
5
|
+
* `node -r securenow/register`; import it directly only for the logger API below.
|
|
6
|
+
* Redaction helpers live in `securenow/nextjs-middleware` and the `securenow redact` CLI.
|
|
67
7
|
*/
|
|
68
|
-
export function redactGraphQLQuery(
|
|
69
|
-
query: string,
|
|
70
|
-
sensitiveFields?: string[]
|
|
71
|
-
): string;
|
|
72
8
|
|
|
73
9
|
/**
|
|
74
10
|
* OpenTelemetry Logger interface
|
|
@@ -86,19 +22,20 @@ export interface LogRecord {
|
|
|
86
22
|
* 5 = DEBUG, 9 = INFO, 13 = WARN, 17 = ERROR
|
|
87
23
|
*/
|
|
88
24
|
severityNumber: number;
|
|
89
|
-
|
|
25
|
+
|
|
90
26
|
/**
|
|
91
27
|
* Human-readable severity text
|
|
92
28
|
*/
|
|
93
29
|
severityText: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | string;
|
|
94
|
-
|
|
30
|
+
|
|
95
31
|
/**
|
|
96
|
-
* Log message body
|
|
32
|
+
* Log message body. Secret shapes (JWTs, Bearer credentials, API keys, card numbers, emails,
|
|
33
|
+
* OTP assignments) are redacted before export unless config.logging.redact is false.
|
|
97
34
|
*/
|
|
98
35
|
body: string;
|
|
99
|
-
|
|
36
|
+
|
|
100
37
|
/**
|
|
101
|
-
* Structured attributes for filtering/searching
|
|
38
|
+
* Structured attributes for filtering/searching (exported as set)
|
|
102
39
|
*/
|
|
103
40
|
attributes?: Record<string, any>;
|
|
104
41
|
}
|
|
@@ -112,75 +49,57 @@ export interface LoggerProvider {
|
|
|
112
49
|
}
|
|
113
50
|
|
|
114
51
|
/**
|
|
115
|
-
* Get a logger
|
|
116
|
-
*
|
|
117
|
-
* @param name - Logger name (e.g., 'my-service', 'auth-module')
|
|
52
|
+
* Get a logger that sends structured logs to SecureNow, correlated with the active trace.
|
|
53
|
+
*
|
|
54
|
+
* @param name - Logger name (e.g., 'my-service', 'auth-module'); defaults to 'default'
|
|
118
55
|
* @param version - Logger version (optional, defaults to '1.0.0')
|
|
119
|
-
* @returns Logger instance or null
|
|
120
|
-
*
|
|
56
|
+
* @returns Logger instance, or null when logging is off (config.logging.enabled=false) or the SDK
|
|
57
|
+
* did not start (SECURENOW_DISABLED=1)
|
|
58
|
+
*
|
|
121
59
|
* @example
|
|
122
60
|
* ```typescript
|
|
123
61
|
* import { getLogger } from 'securenow/tracing';
|
|
124
|
-
*
|
|
62
|
+
*
|
|
125
63
|
* const logger = getLogger('my-service', '1.0.0');
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* userId: 123,
|
|
134
|
-
* username: 'john',
|
|
135
|
-
* },
|
|
136
|
-
* });
|
|
137
|
-
* }
|
|
64
|
+
*
|
|
65
|
+
* logger?.emit({
|
|
66
|
+
* severityNumber: 9,
|
|
67
|
+
* severityText: 'INFO',
|
|
68
|
+
* body: 'User logged in',
|
|
69
|
+
* attributes: { userId: 123 },
|
|
70
|
+
* });
|
|
138
71
|
* ```
|
|
139
72
|
*/
|
|
140
73
|
export function getLogger(name?: string, version?: string): Logger | null;
|
|
141
74
|
|
|
142
75
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* @returns true when logging resolves enabled from .securenow/credentials.json defaults
|
|
146
|
-
*
|
|
147
|
-
* @example
|
|
148
|
-
* ```typescript
|
|
149
|
-
* import { isLoggingEnabled } from 'securenow/tracing';
|
|
150
|
-
*
|
|
151
|
-
* if (isLoggingEnabled()) {
|
|
152
|
-
* console.log('Logging is enabled');
|
|
153
|
-
* }
|
|
154
|
-
* ```
|
|
76
|
+
* Whether logs are exported (config.logging.enabled, default true; false when the SDK did not start).
|
|
155
77
|
*/
|
|
156
78
|
export function isLoggingEnabled(): boolean;
|
|
157
79
|
|
|
158
80
|
/**
|
|
159
|
-
* The OpenTelemetry LoggerProvider instance (
|
|
160
|
-
* Use getLogger() instead of accessing this directly
|
|
81
|
+
* The OpenTelemetry LoggerProvider instance (null when logging is off).
|
|
82
|
+
* Use getLogger() instead of accessing this directly.
|
|
161
83
|
*/
|
|
162
84
|
export const loggerProvider: LoggerProvider | null;
|
|
163
85
|
|
|
164
86
|
/**
|
|
165
|
-
* Configuration:
|
|
166
|
-
*
|
|
167
|
-
* Local development reads .securenow/credentials.json first. Run
|
|
168
|
-
* `npx securenow login` to write app identity/runtime API key and
|
|
169
|
-
* `npx securenow init` to ensure secure defaults and explanations.
|
|
87
|
+
* Configuration: .securenow/runtime.json, written by `npx securenow login`; `npx securenow init`
|
|
88
|
+
* adds the secure defaults with explanations.
|
|
170
89
|
*
|
|
171
|
-
* Production
|
|
172
|
-
*
|
|
173
|
-
*
|
|
90
|
+
* Production: `npx securenow credentials runtime --env production`, then mount/copy the file to
|
|
91
|
+
* .securenow/credentials.json in the running app. Hosts without file mounts store
|
|
92
|
+
* `npx securenow credentials runtime --env production --format env` (SECURENOW_CREDENTIALS) as a
|
|
93
|
+
* secret environment variable. No other SECURENOW_* variable is read; SECURENOW_DISABLED=1
|
|
94
|
+
* turns the SDK off (tests).
|
|
174
95
|
*
|
|
175
96
|
* Main config fields:
|
|
176
|
-
* - app.key / app.name
|
|
177
|
-
* - config.otel.endpoint only for advanced/self-hosted collector overrides
|
|
178
|
-
* - apiKey
|
|
97
|
+
* - app.key / app.name, apiKey (runtime API key)
|
|
179
98
|
* - config.runtime.deploymentEnvironment
|
|
180
|
-
* - config.logging.enabled
|
|
99
|
+
* - config.logging.enabled / redact
|
|
181
100
|
* - config.capture.body / multipart / maxBodySize / sensitiveFields
|
|
182
|
-
* - config.otel.endpoint / tracesEndpoint / logsEndpoint / headers
|
|
183
|
-
* - config.
|
|
184
|
-
*
|
|
185
|
-
*
|
|
101
|
+
* - config.otel.endpoint / tracesEndpoint / logsEndpoint / headers (self-hosted collectors only)
|
|
102
|
+
* - config.networking.trustedProxies
|
|
103
|
+
* - config.firewall.* (failMode, statusCode, tcp, iptables, cloud, rateLimitStore, ...)
|
|
104
|
+
* - config.sessions.guard / capture / cookieNames
|
|
186
105
|
*/
|