securenow 5.12.1 → 5.14.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/cli/firewall.js +23 -4
- package/cli/init.js +170 -69
- package/cli/security.js +102 -0
- package/cli.js +19 -2
- package/firewall-only.js +38 -0
- package/firewall-tcp.js +20 -4
- package/firewall.js +206 -17
- package/nextjs-webpack-config.js +88 -53
- package/nextjs.js +21 -22
- package/nuxt-server-plugin.mjs +23 -0
- package/package.json +5 -1
package/cli/firewall.js
CHANGED
|
@@ -23,10 +23,17 @@ async function status(args, flags) {
|
|
|
23
23
|
ui.keyValue([
|
|
24
24
|
['Blocked IPs', `${data.totalIps} total (${data.exactCount} exact + ${data.cidrCount} CIDR ranges)`],
|
|
25
25
|
['Last updated', data.updatedAt || 'unknown'],
|
|
26
|
+
['Allowed IPs', data.allowlistCount != null ? `${data.allowlistCount} total (${data.allowlistExactCount} exact + ${data.allowlistCidrCount} CIDR ranges)` : '0'],
|
|
27
|
+
['Allowlist updated', data.allowlistUpdatedAt || 'never'],
|
|
26
28
|
['Sync TTL', `${data.ttl || 60}s`],
|
|
27
29
|
]);
|
|
30
|
+
if (data.allowlistCount > 0) {
|
|
31
|
+
console.log('');
|
|
32
|
+
console.log(` ${ui.c.yellow('!')} Allowlist is active — only ${data.allowlistCount} IP(s) can reach your app`);
|
|
33
|
+
}
|
|
28
34
|
console.log('');
|
|
29
35
|
console.log(` ${ui.c.dim('Manage your blocklist:')} securenow blocklist`);
|
|
36
|
+
console.log(` ${ui.c.dim('Manage your allowlist:')} securenow allowlist`);
|
|
30
37
|
console.log(` ${ui.c.dim('Test an IP:')} securenow firewall test-ip <ip>`);
|
|
31
38
|
console.log('');
|
|
32
39
|
} catch (err) {
|
|
@@ -63,14 +70,26 @@ async function testIp(args, flags) {
|
|
|
63
70
|
|
|
64
71
|
console.log('');
|
|
65
72
|
if (data.blocked) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
console.log(` ${ui.c.dim(
|
|
73
|
+
if (data.allowlistActive && !data.allowlisted) {
|
|
74
|
+
console.log(` ${ui.c.bold(ui.c.red('BLOCKED'))} — ${ip} is not on the allowlist`);
|
|
75
|
+
console.log(` ${ui.c.dim('Allowlist is active — only listed IPs are permitted')}`);
|
|
76
|
+
} else {
|
|
77
|
+
console.log(` ${ui.c.bold(ui.c.red('BLOCKED'))} — ${ip} is in the blocklist`);
|
|
78
|
+
if (data.matchedEntry && data.matchedEntry !== ip) {
|
|
79
|
+
console.log(` ${ui.c.dim(`Matched by: ${data.matchedEntry}`)}`);
|
|
80
|
+
}
|
|
69
81
|
}
|
|
70
82
|
} else {
|
|
71
|
-
|
|
83
|
+
if (data.allowlisted) {
|
|
84
|
+
console.log(` ${ui.c.bold(ui.c.green('ALLOWED'))} — ${ip} is on the allowlist`);
|
|
85
|
+
} else {
|
|
86
|
+
console.log(` ${ui.c.bold(ui.c.green('ALLOWED'))} — ${ip} is not in the blocklist`);
|
|
87
|
+
}
|
|
72
88
|
}
|
|
73
89
|
console.log(` ${ui.c.dim(`Blocklist contains ${data.totalBlockedIps} entries`)}`);
|
|
90
|
+
if (data.allowlistActive) {
|
|
91
|
+
console.log(` ${ui.c.dim('Allowlist is active')}`);
|
|
92
|
+
}
|
|
74
93
|
console.log('');
|
|
75
94
|
} catch (err) {
|
|
76
95
|
s.fail('Failed to test IP');
|
package/cli/init.js
CHANGED
|
@@ -4,97 +4,198 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const ui = require('./ui');
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
|
|
7
|
+
const INSTRUMENTATION_JS = `export async function register() {
|
|
8
|
+
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
9
|
+
const { registerSecureNow } = require('securenow/nextjs');
|
|
10
|
+
registerSecureNow();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
const INSTRUMENTATION_TS = `export async function register() {
|
|
16
|
+
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
17
|
+
const { registerSecureNow } = require('securenow/nextjs');
|
|
18
|
+
registerSecureNow();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
function detectProject(dir) {
|
|
24
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
25
|
+
if (!fs.existsSync(pkgPath)) return { framework: 'unknown' };
|
|
9
26
|
|
|
10
|
-
|
|
11
|
-
|
|
27
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
28
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
29
|
+
|
|
30
|
+
if (allDeps.next) return { framework: 'nextjs', pkg, pkgPath, nextVersion: allDeps.next };
|
|
31
|
+
if (allDeps.nuxt) return { framework: 'nuxt', pkg, pkgPath };
|
|
32
|
+
if (allDeps.express) return { framework: 'express', pkg, pkgPath };
|
|
33
|
+
if (allDeps.fastify) return { framework: 'fastify', pkg, pkgPath };
|
|
34
|
+
if (allDeps.koa) return { framework: 'koa', pkg, pkgPath };
|
|
35
|
+
if (allDeps.hapi || allDeps['@hapi/hapi']) return { framework: 'hapi', pkg, pkgPath };
|
|
36
|
+
return { framework: 'node', pkg, pkgPath };
|
|
12
37
|
}
|
|
13
|
-
`,
|
|
14
|
-
javascript: `const { registerSecureNow } = require('securenow/nextjs');
|
|
15
38
|
|
|
16
|
-
|
|
17
|
-
|
|
39
|
+
function findInstrumentationFile(dir) {
|
|
40
|
+
for (const name of ['instrumentation.ts', 'instrumentation.js', 'src/instrumentation.ts', 'src/instrumentation.js']) {
|
|
41
|
+
const p = path.join(dir, name);
|
|
42
|
+
if (fs.existsSync(p)) return p;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
18
45
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
# OTEL_LOG_LEVEL=info
|
|
25
|
-
`,
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
function isNextJsProject(dir) {
|
|
29
|
-
try {
|
|
30
|
-
const pkgPath = path.join(dir, 'package.json');
|
|
31
|
-
if (!fs.existsSync(pkgPath)) return false;
|
|
32
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
33
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
34
|
-
return !!deps.next;
|
|
35
|
-
} catch {
|
|
36
|
-
return false;
|
|
46
|
+
|
|
47
|
+
function findNextConfig(dir) {
|
|
48
|
+
for (const name of ['next.config.js', 'next.config.mjs', 'next.config.ts']) {
|
|
49
|
+
const p = path.join(dir, name);
|
|
50
|
+
if (fs.existsSync(p)) return p;
|
|
37
51
|
}
|
|
52
|
+
return null;
|
|
38
53
|
}
|
|
39
54
|
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
function hasTypeScript(dir) {
|
|
56
|
+
return fs.existsSync(path.join(dir, 'tsconfig.json'));
|
|
57
|
+
}
|
|
42
58
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
59
|
+
async function init(_args, flags) {
|
|
60
|
+
const dir = process.cwd();
|
|
61
|
+
const project = detectProject(dir);
|
|
46
62
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
ui.
|
|
63
|
+
ui.header('SecureNow Project Setup');
|
|
64
|
+
|
|
65
|
+
if (project.framework === 'unknown') {
|
|
66
|
+
ui.error('No package.json found. Run this command in your project root.');
|
|
51
67
|
process.exit(1);
|
|
52
68
|
}
|
|
53
69
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
70
|
+
ui.info(`Detected framework: ${ui.bold(project.framework)}`);
|
|
71
|
+
|
|
72
|
+
if (project.framework === 'nextjs') {
|
|
73
|
+
await initNextJs(dir, project, flags);
|
|
74
|
+
} else if (project.framework === 'nuxt') {
|
|
75
|
+
initNuxt(dir, project);
|
|
76
|
+
} else {
|
|
77
|
+
initNode(dir, project);
|
|
78
|
+
}
|
|
58
79
|
|
|
59
|
-
|
|
60
|
-
const filePath = useSrc ? path.join(cwd, 'src', fileName) : path.join(cwd, fileName);
|
|
80
|
+
initEnv(dir, flags);
|
|
61
81
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
82
|
+
console.log('');
|
|
83
|
+
ui.success('Setup complete! Run your app to verify.');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function initNextJs(dir, project, flags) {
|
|
87
|
+
const useTs = hasTypeScript(dir);
|
|
88
|
+
const ext = useTs ? 'ts' : 'js';
|
|
89
|
+
|
|
90
|
+
const existing = findInstrumentationFile(dir);
|
|
91
|
+
if (existing) {
|
|
92
|
+
ui.info(`instrumentation file already exists: ${path.relative(dir, existing)}`);
|
|
93
|
+
} else {
|
|
94
|
+
const filePath = path.join(dir, `instrumentation.${ext}`);
|
|
95
|
+
const content = useTs ? INSTRUMENTATION_TS : INSTRUMENTATION_JS;
|
|
96
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
97
|
+
ui.success(`Created instrumentation.${ext}`);
|
|
65
98
|
}
|
|
66
99
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
100
|
+
const configPath = findNextConfig(dir);
|
|
101
|
+
if (configPath) {
|
|
102
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
103
|
+
if (content.includes('withSecureNow')) {
|
|
104
|
+
ui.info('next.config already uses withSecureNow — skipping');
|
|
105
|
+
} else if (content.includes('serverExternalPackages') && content.includes('securenow')) {
|
|
106
|
+
ui.info('next.config already externalizes securenow — skipping');
|
|
107
|
+
} else if (content.includes('serverComponentsExternalPackages') && content.includes('securenow')) {
|
|
108
|
+
ui.info('next.config already externalizes securenow — skipping');
|
|
109
|
+
} else {
|
|
110
|
+
ui.warn(`Update your ${path.basename(configPath)} to use withSecureNow():`);
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log(` const { withSecureNow } = require('securenow/nextjs-webpack-config');`);
|
|
113
|
+
console.log(` module.exports = withSecureNow({ /* your config */ });`);
|
|
114
|
+
console.log('');
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
const newConfigPath = path.join(dir, 'next.config.js');
|
|
118
|
+
fs.writeFileSync(newConfigPath, `const { withSecureNow } = require('securenow/nextjs-webpack-config');\n\nmodule.exports = withSecureNow({\n reactStrictMode: true,\n});\n`, 'utf8');
|
|
119
|
+
ui.success('Created next.config.js with withSecureNow()');
|
|
75
120
|
}
|
|
121
|
+
}
|
|
76
122
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
123
|
+
function initNuxt(dir, project) {
|
|
124
|
+
const configPath = path.join(dir, 'nuxt.config.ts');
|
|
125
|
+
if (fs.existsSync(configPath)) {
|
|
126
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
127
|
+
if (content.includes('securenow/nuxt')) {
|
|
128
|
+
ui.info('nuxt.config already references securenow/nuxt — skipping');
|
|
129
|
+
} else {
|
|
130
|
+
ui.warn('Add securenow/nuxt to your nuxt.config modules:');
|
|
131
|
+
console.log('');
|
|
132
|
+
console.log(" modules: ['securenow/nuxt'],");
|
|
133
|
+
console.log('');
|
|
84
134
|
}
|
|
85
135
|
} else {
|
|
86
|
-
ui.
|
|
136
|
+
ui.warn('Add securenow/nuxt to your nuxt.config modules array.');
|
|
87
137
|
}
|
|
138
|
+
}
|
|
88
139
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
140
|
+
function initNode(dir, project) {
|
|
141
|
+
const pkg = project.pkg;
|
|
142
|
+
const scripts = pkg.scripts || {};
|
|
143
|
+
|
|
144
|
+
const startScript = scripts.start || '';
|
|
145
|
+
if (startScript.includes('securenow/register')) {
|
|
146
|
+
ui.info('start script already uses securenow/register — skipping');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (startScript) {
|
|
151
|
+
ui.warn('Update your start script to include the securenow preload:');
|
|
152
|
+
console.log('');
|
|
153
|
+
if (startScript.includes('node ')) {
|
|
154
|
+
const updated = startScript.replace('node ', 'node -r securenow/register ');
|
|
155
|
+
console.log(` "start": "${updated}"`);
|
|
156
|
+
} else {
|
|
157
|
+
console.log(` "start": "node -r securenow/register ${startScript.replace(/^node\s+/, '')}"`);
|
|
158
|
+
}
|
|
159
|
+
console.log('');
|
|
160
|
+
} else {
|
|
161
|
+
ui.warn('Add a start script with the securenow preload:');
|
|
162
|
+
console.log('');
|
|
163
|
+
console.log(' "start": "node -r securenow/register src/index.js"');
|
|
164
|
+
console.log('');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function initEnv(dir, flags) {
|
|
169
|
+
const envFiles = ['.env', '.env.local'];
|
|
170
|
+
let envPath = null;
|
|
171
|
+
|
|
172
|
+
for (const f of envFiles) {
|
|
173
|
+
const p = path.join(dir, f);
|
|
174
|
+
if (fs.existsSync(p)) {
|
|
175
|
+
const content = fs.readFileSync(p, 'utf8');
|
|
176
|
+
if (content.includes('SECURENOW_API_KEY')) {
|
|
177
|
+
ui.info(`SECURENOW_API_KEY already set in ${f}`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
envPath = p;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!envPath) envPath = path.join(dir, '.env.local');
|
|
186
|
+
|
|
187
|
+
const apiKey = flags.key || flags['api-key'] || '';
|
|
188
|
+
if (apiKey) {
|
|
189
|
+
const existing = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf8') : '';
|
|
190
|
+
const sep = existing && !existing.endsWith('\n') ? '\n' : '';
|
|
191
|
+
fs.appendFileSync(envPath, `${sep}SECURENOW_API_KEY=${apiKey}\n`, 'utf8');
|
|
192
|
+
ui.success(`Added SECURENOW_API_KEY to ${path.basename(envPath)}`);
|
|
193
|
+
} else {
|
|
194
|
+
ui.warn(`Add your API key to ${path.basename(envPath)}:`);
|
|
195
|
+
console.log('');
|
|
196
|
+
console.log(' SECURENOW_API_KEY=snk_live_...');
|
|
197
|
+
console.log('');
|
|
198
|
+
}
|
|
98
199
|
}
|
|
99
200
|
|
|
100
201
|
module.exports = { init };
|
package/cli/security.js
CHANGED
|
@@ -197,6 +197,104 @@ async function blocklistStats(args, flags) {
|
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
// ── Allowlist ──
|
|
201
|
+
|
|
202
|
+
async function allowlistList(args, flags) {
|
|
203
|
+
requireAuth();
|
|
204
|
+
const s = ui.spinner('Fetching allowlist');
|
|
205
|
+
try {
|
|
206
|
+
const data = await api.get('/allowlist');
|
|
207
|
+
const items = data.allowedIps || [];
|
|
208
|
+
s.stop(`Found ${items.length} allowed IP${items.length !== 1 ? 's' : ''}${data.total ? ` (${data.total} total)` : ''}`);
|
|
209
|
+
|
|
210
|
+
if (flags.json) { ui.json(data); return; }
|
|
211
|
+
|
|
212
|
+
console.log('');
|
|
213
|
+
const rows = items.map(a => [
|
|
214
|
+
ui.c.dim(ui.truncate(a._id, 12)),
|
|
215
|
+
ui.c.green(a.ip || '—'),
|
|
216
|
+
a.label || ui.c.dim('—'),
|
|
217
|
+
ui.truncate(a.reason || '', 40),
|
|
218
|
+
ui.timeAgo(a.createdAt),
|
|
219
|
+
a.expiresAt ? new Date(a.expiresAt).toLocaleDateString() : ui.c.dim('permanent'),
|
|
220
|
+
]);
|
|
221
|
+
ui.table(['ID', 'IP/CIDR', 'Label', 'Reason', 'Added', 'Expires'], rows);
|
|
222
|
+
console.log('');
|
|
223
|
+
} catch (err) {
|
|
224
|
+
s.fail('Failed to fetch allowlist');
|
|
225
|
+
throw err;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function allowlistAdd(args, flags) {
|
|
230
|
+
requireAuth();
|
|
231
|
+
let ip = args[0];
|
|
232
|
+
if (!ip) {
|
|
233
|
+
ip = await ui.prompt('IP address or CIDR to allow');
|
|
234
|
+
if (!ip) { ui.error('IP is required'); process.exit(1); }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const body = { ip };
|
|
238
|
+
if (flags.label) body.label = flags.label;
|
|
239
|
+
if (flags.reason) body.reason = flags.reason;
|
|
240
|
+
|
|
241
|
+
const s = ui.spinner(`Allowing ${ip}`);
|
|
242
|
+
try {
|
|
243
|
+
await api.post('/allowlist', body);
|
|
244
|
+
s.stop(`${ip} added to allowlist`);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
s.fail('Failed to add to allowlist');
|
|
247
|
+
throw err;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function allowlistRemove(args, flags) {
|
|
252
|
+
requireAuth();
|
|
253
|
+
const id = args[0];
|
|
254
|
+
if (!id) {
|
|
255
|
+
ui.error('Allowlist entry ID required. Usage: securenow allowlist remove <id>');
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!flags.force && !flags.yes) {
|
|
260
|
+
const ok = await ui.confirm('Remove this IP from the allowlist?');
|
|
261
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const s = ui.spinner('Removing from allowlist');
|
|
265
|
+
try {
|
|
266
|
+
await api.delete(`/allowlist/${id}`);
|
|
267
|
+
s.stop('Removed from allowlist');
|
|
268
|
+
} catch (err) {
|
|
269
|
+
s.fail('Failed to remove from allowlist');
|
|
270
|
+
throw err;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function allowlistStats(args, flags) {
|
|
275
|
+
requireAuth();
|
|
276
|
+
const s = ui.spinner('Fetching allowlist stats');
|
|
277
|
+
try {
|
|
278
|
+
const data = await api.get('/allowlist/stats');
|
|
279
|
+
const stats = data.stats || data;
|
|
280
|
+
s.stop('Stats loaded');
|
|
281
|
+
|
|
282
|
+
if (flags.json) { ui.json(stats); return; }
|
|
283
|
+
|
|
284
|
+
console.log('');
|
|
285
|
+
ui.heading('Allowlist Statistics');
|
|
286
|
+
console.log('');
|
|
287
|
+
ui.keyValue([
|
|
288
|
+
['Total Active', String(stats.totalActive ?? '—')],
|
|
289
|
+
['Total Removed', String(stats.totalRemoved ?? '—')],
|
|
290
|
+
]);
|
|
291
|
+
console.log('');
|
|
292
|
+
} catch (err) {
|
|
293
|
+
s.fail('Failed to fetch stats');
|
|
294
|
+
throw err;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
200
298
|
// ── Trusted IPs ──
|
|
201
299
|
|
|
202
300
|
async function trustedList(args, flags) {
|
|
@@ -810,6 +908,10 @@ module.exports = {
|
|
|
810
908
|
blocklistAdd,
|
|
811
909
|
blocklistRemove,
|
|
812
910
|
blocklistStats,
|
|
911
|
+
allowlistList,
|
|
912
|
+
allowlistAdd,
|
|
913
|
+
allowlistRemove,
|
|
914
|
+
allowlistStats,
|
|
813
915
|
trustedList,
|
|
814
916
|
trustedAdd,
|
|
815
917
|
trustedRemove,
|
package/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const ui = require('./cli/ui');
|
|
@@ -39,6 +39,12 @@ function parseArgs(argv) {
|
|
|
39
39
|
// ── Command Registry ──
|
|
40
40
|
|
|
41
41
|
const COMMANDS = {
|
|
42
|
+
init: {
|
|
43
|
+
desc: 'Set up SecureNow in the current project (instrumentation, config, env)',
|
|
44
|
+
usage: 'securenow init [--key <API_KEY>]',
|
|
45
|
+
flags: { key: 'API key to write to .env', 'api-key': 'Alias for --key' },
|
|
46
|
+
run: (a, f) => require('./cli/init').init(a, f),
|
|
47
|
+
},
|
|
42
48
|
login: {
|
|
43
49
|
desc: 'Authenticate with SecureNow',
|
|
44
50
|
usage: 'securenow login [--token <TOKEN>]',
|
|
@@ -156,6 +162,17 @@ const COMMANDS = {
|
|
|
156
162
|
},
|
|
157
163
|
defaultSub: 'list',
|
|
158
164
|
},
|
|
165
|
+
allowlist: {
|
|
166
|
+
desc: 'Manage IP allowlist (only allow listed IPs)',
|
|
167
|
+
usage: 'securenow allowlist <subcommand> [options]',
|
|
168
|
+
sub: {
|
|
169
|
+
list: { desc: 'List allowed IPs', run: (a, f) => require('./cli/security').allowlistList(a, f) },
|
|
170
|
+
add: { desc: 'Allow an IP', usage: 'securenow allowlist add <ip> [--label <label>] [--reason <reason>]', run: (a, f) => require('./cli/security').allowlistAdd(a, f) },
|
|
171
|
+
remove: { desc: 'Remove an allowed IP', usage: 'securenow allowlist remove <id>', run: (a, f) => require('./cli/security').allowlistRemove(a, f) },
|
|
172
|
+
stats: { desc: 'Allowlist statistics', run: (a, f) => require('./cli/security').allowlistStats(a, f) },
|
|
173
|
+
},
|
|
174
|
+
defaultSub: 'list',
|
|
175
|
+
},
|
|
159
176
|
trusted: {
|
|
160
177
|
desc: 'Manage trusted IPs',
|
|
161
178
|
usage: 'securenow trusted <subcommand> [options]',
|
|
@@ -334,7 +351,7 @@ function showHelp(commandName) {
|
|
|
334
351
|
'Detect & Respond': ['issues', 'notifications', 'alerts', 'fp'],
|
|
335
352
|
'Investigate': ['ip', 'forensics', 'api-map'],
|
|
336
353
|
'Firewall': ['firewall'],
|
|
337
|
-
'Remediation': ['blocklist', 'trusted'],
|
|
354
|
+
'Remediation': ['blocklist', 'allowlist', 'trusted'],
|
|
338
355
|
'Settings': ['instances', 'config', 'version'],
|
|
339
356
|
};
|
|
340
357
|
|
package/firewall-only.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Standalone firewall preload — no OpenTelemetry, no tracing.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node -r securenow/firewall-only app.js
|
|
8
|
+
* NODE_OPTIONS='-r securenow/firewall-only' next start
|
|
9
|
+
*
|
|
10
|
+
* Reads .env via dotenv (if installed), then initialises the HTTP-level
|
|
11
|
+
* firewall when SECURENOW_API_KEY is present.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
try { require('dotenv').config(); } catch (_) {}
|
|
15
|
+
|
|
16
|
+
const env = (k) =>
|
|
17
|
+
process.env[k] ?? process.env[k.toUpperCase()] ?? process.env[k.toLowerCase()];
|
|
18
|
+
|
|
19
|
+
const firewallApiKey = env('SECURENOW_API_KEY');
|
|
20
|
+
|
|
21
|
+
if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
|
|
22
|
+
require('./firewall').init({
|
|
23
|
+
apiKey: firewallApiKey,
|
|
24
|
+
apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
|
|
25
|
+
versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
|
|
26
|
+
syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
|
|
27
|
+
failMode: env('SECURENOW_FIREWALL_FAIL_MODE') || 'open',
|
|
28
|
+
statusCode: parseInt(env('SECURENOW_FIREWALL_STATUS_CODE'), 10) || 403,
|
|
29
|
+
log: env('SECURENOW_FIREWALL_LOG') !== '0',
|
|
30
|
+
tcp: env('SECURENOW_FIREWALL_TCP') === '1',
|
|
31
|
+
iptables: env('SECURENOW_FIREWALL_IPTABLES') === '1',
|
|
32
|
+
cloud: env('SECURENOW_FIREWALL_CLOUD') || null,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const shutdown = () => { try { require('./firewall').shutdown(); } catch (_) {} };
|
|
36
|
+
process.on('SIGINT', shutdown);
|
|
37
|
+
process.on('SIGTERM', shutdown);
|
|
38
|
+
}
|
package/firewall-tcp.js
CHANGED
|
@@ -13,20 +13,35 @@ const net = require('net');
|
|
|
13
13
|
const { resolveSocketIp, isFromTrustedProxy } = require('./resolve-ip');
|
|
14
14
|
|
|
15
15
|
let _getMatcher = null;
|
|
16
|
+
let _getAllowlistMatcher = null;
|
|
16
17
|
let _options = null;
|
|
17
18
|
let _patched = false;
|
|
18
19
|
const _origListen = net.Server.prototype.listen;
|
|
19
20
|
|
|
20
21
|
function onConnection(socket) {
|
|
21
|
-
const matcher = _getMatcher();
|
|
22
|
-
if (!matcher) return;
|
|
23
|
-
|
|
24
22
|
const ip = resolveSocketIp(socket);
|
|
25
23
|
|
|
26
24
|
// Skip if the connection is from a trusted proxy — Layer 1 will handle it
|
|
27
25
|
// with proper X-Forwarded-For resolution
|
|
28
26
|
if (isFromTrustedProxy(ip) || isFromTrustedProxy('::ffff:' + ip)) return;
|
|
29
27
|
|
|
28
|
+
// Allowlist check: if active, only listed IPs pass
|
|
29
|
+
const allowlistMatcher = _getAllowlistMatcher ? _getAllowlistMatcher() : null;
|
|
30
|
+
if (allowlistMatcher && allowlistMatcher.stats().total > 0) {
|
|
31
|
+
if (!allowlistMatcher.isBlocked(ip)) {
|
|
32
|
+
if (_options && _options.log) {
|
|
33
|
+
console.log('[securenow] Firewall: blocked %s via TCP (not in allowlist)', ip);
|
|
34
|
+
}
|
|
35
|
+
socket.destroy();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
return; // on allowlist — allow
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Blocklist check
|
|
42
|
+
const matcher = _getMatcher();
|
|
43
|
+
if (!matcher) return;
|
|
44
|
+
|
|
30
45
|
if (matcher.isBlocked(ip)) {
|
|
31
46
|
if (_options && _options.log) {
|
|
32
47
|
console.log('[securenow] Firewall: blocked %s via TCP (socket destroyed)', ip);
|
|
@@ -35,8 +50,9 @@ function onConnection(socket) {
|
|
|
35
50
|
}
|
|
36
51
|
}
|
|
37
52
|
|
|
38
|
-
function init(getMatcher, options) {
|
|
53
|
+
function init(getMatcher, options, getAllowlistMatcher) {
|
|
39
54
|
_getMatcher = getMatcher;
|
|
55
|
+
_getAllowlistMatcher = getAllowlistMatcher || null;
|
|
40
56
|
_options = options;
|
|
41
57
|
|
|
42
58
|
if (_patched) return;
|
package/firewall.js
CHANGED
|
@@ -17,6 +17,15 @@ let _consecutiveErrors = 0;
|
|
|
17
17
|
let _layers = [];
|
|
18
18
|
let _rawIps = [];
|
|
19
19
|
let _stats = { syncs: 0, blocked: 0, allowed: 0, versionChecks: 0, errors: 0 };
|
|
20
|
+
let _localhostFallbackTried = false;
|
|
21
|
+
|
|
22
|
+
// Allowlist state
|
|
23
|
+
let _allowlistMatcher = null;
|
|
24
|
+
let _allowlistRawIps = [];
|
|
25
|
+
let _lastAllowlistModified = null;
|
|
26
|
+
let _lastAllowlistVersion = null;
|
|
27
|
+
let _lastAllowlistSyncEtag = null;
|
|
28
|
+
let _allowlistVersionTimer = null;
|
|
20
29
|
|
|
21
30
|
// ────── Blocklist Sync ──────
|
|
22
31
|
|
|
@@ -87,6 +96,136 @@ function notifyLayers(ips) {
|
|
|
87
96
|
}
|
|
88
97
|
}
|
|
89
98
|
|
|
99
|
+
// ────── Allowlist Sync ──────
|
|
100
|
+
|
|
101
|
+
function syncAllowlist(callback) {
|
|
102
|
+
const url = buildUrl(_options.apiUrl, '/firewall/allowlist');
|
|
103
|
+
const mod = url.startsWith('https') ? https : http;
|
|
104
|
+
const parsed = new URL(url);
|
|
105
|
+
|
|
106
|
+
const reqOptions = {
|
|
107
|
+
hostname: parsed.hostname,
|
|
108
|
+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
109
|
+
path: parsed.pathname + parsed.search,
|
|
110
|
+
method: 'GET',
|
|
111
|
+
headers: {
|
|
112
|
+
'Authorization': `Bearer ${_options.apiKey}`,
|
|
113
|
+
'User-Agent': 'securenow-firewall-sdk',
|
|
114
|
+
},
|
|
115
|
+
timeout: 10000,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (_lastAllowlistSyncEtag) {
|
|
119
|
+
reqOptions.headers['If-None-Match'] = _lastAllowlistSyncEtag;
|
|
120
|
+
} else if (_lastAllowlistModified) {
|
|
121
|
+
reqOptions.headers['If-Modified-Since'] = _lastAllowlistModified;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const req = mod.request(reqOptions, (res) => {
|
|
125
|
+
if (res.statusCode === 304) {
|
|
126
|
+
callback(null, false);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let data = '';
|
|
131
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
132
|
+
res.on('end', () => {
|
|
133
|
+
if (res.statusCode !== 200) {
|
|
134
|
+
callback(new Error(`API returned ${res.statusCode}: ${data.slice(0, 200)}`));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const body = JSON.parse(data);
|
|
139
|
+
const ips = body.ips || [];
|
|
140
|
+
_allowlistRawIps = ips;
|
|
141
|
+
_allowlistMatcher = createMatcher(ips);
|
|
142
|
+
_lastAllowlistModified = res.headers['last-modified'] || null;
|
|
143
|
+
if (res.headers['etag']) _lastAllowlistSyncEtag = res.headers['etag'];
|
|
144
|
+
callback(null, true, _allowlistMatcher.stats());
|
|
145
|
+
} catch (e) {
|
|
146
|
+
callback(new Error(`Failed to parse allowlist response: ${e.message}`));
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
req.on('error', (err) => callback(err));
|
|
152
|
+
req.on('timeout', () => { req.destroy(); callback(new Error('Allowlist sync request timed out')); });
|
|
153
|
+
req.end();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function checkAllowlistVersion(callback) {
|
|
157
|
+
const url = buildUrl(_options.apiUrl, '/firewall/allowlist/version');
|
|
158
|
+
const mod = url.startsWith('https') ? https : http;
|
|
159
|
+
const parsed = new URL(url);
|
|
160
|
+
|
|
161
|
+
const headers = {
|
|
162
|
+
'Authorization': `Bearer ${_options.apiKey}`,
|
|
163
|
+
'User-Agent': 'securenow-firewall-sdk',
|
|
164
|
+
};
|
|
165
|
+
if (_lastAllowlistVersion) headers['If-None-Match'] = _lastAllowlistVersion;
|
|
166
|
+
|
|
167
|
+
const req = mod.request({
|
|
168
|
+
hostname: parsed.hostname,
|
|
169
|
+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
170
|
+
path: parsed.pathname + parsed.search,
|
|
171
|
+
method: 'GET',
|
|
172
|
+
headers,
|
|
173
|
+
timeout: 5000,
|
|
174
|
+
}, (res) => {
|
|
175
|
+
if (res.statusCode === 304) {
|
|
176
|
+
res.resume();
|
|
177
|
+
callback(null, false);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let data = '';
|
|
182
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
183
|
+
res.on('end', () => {
|
|
184
|
+
if (res.statusCode !== 200) {
|
|
185
|
+
callback(null, false);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
const body = JSON.parse(data);
|
|
190
|
+
const version = body.version || null;
|
|
191
|
+
const changed = version !== _lastAllowlistVersion;
|
|
192
|
+
if (changed) _lastAllowlistVersion = version;
|
|
193
|
+
callback(null, changed);
|
|
194
|
+
} catch (_e) { callback(null, false); }
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
req.on('error', () => { callback(null, false); });
|
|
199
|
+
req.on('timeout', () => { req.destroy(); callback(null, false); });
|
|
200
|
+
req.end();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function doFullAllowlistSync() {
|
|
204
|
+
syncAllowlist((err, changed, stats) => {
|
|
205
|
+
if (err) {
|
|
206
|
+
if (_options.log) console.warn('[securenow] Firewall: allowlist sync failed:', err.message);
|
|
207
|
+
} else if (changed && stats && _options.log) {
|
|
208
|
+
console.log('[securenow] Firewall: re-synced %d allowed IPs', stats.total);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function scheduleNextAllowlistVersionCheck() {
|
|
214
|
+
const baseMs = (_options.versionCheckInterval || 10) * 1000;
|
|
215
|
+
const delayMs = jitter(baseMs);
|
|
216
|
+
|
|
217
|
+
_allowlistVersionTimer = setTimeout(() => {
|
|
218
|
+
checkAllowlistVersion((_err, changed) => {
|
|
219
|
+
if (changed) {
|
|
220
|
+
if (_options.log) console.log('[securenow] Firewall: allowlist version changed, syncing…');
|
|
221
|
+
doFullAllowlistSync();
|
|
222
|
+
}
|
|
223
|
+
scheduleNextAllowlistVersionCheck();
|
|
224
|
+
});
|
|
225
|
+
}, delayMs);
|
|
226
|
+
if (_allowlistVersionTimer.unref) _allowlistVersionTimer.unref();
|
|
227
|
+
}
|
|
228
|
+
|
|
90
229
|
function checkVersion(callback) {
|
|
91
230
|
const url = buildUrl(_options.apiUrl, '/firewall/blocklist/version');
|
|
92
231
|
const mod = url.startsWith('https') ? https : http;
|
|
@@ -178,6 +317,16 @@ function startSyncLoop() {
|
|
|
178
317
|
function initialSync() {
|
|
179
318
|
syncBlocklist((err, changed, stats) => {
|
|
180
319
|
if (err) {
|
|
320
|
+
const isConnErr = /ECONNREFUSED|ENOTFOUND|timed out/i.test(err.message);
|
|
321
|
+
if (isConnErr && !_localhostFallbackTried && _options.apiUrl !== 'http://localhost:4000') {
|
|
322
|
+
_localhostFallbackTried = true;
|
|
323
|
+
const origUrl = _options.apiUrl;
|
|
324
|
+
_options.apiUrl = 'http://localhost:4000';
|
|
325
|
+
if (_options.log) console.log('[securenow] Firewall: %s unreachable, trying http://localhost:4000', origUrl);
|
|
326
|
+
const retryTimer = setTimeout(initialSync, 1000);
|
|
327
|
+
if (retryTimer.unref) retryTimer.unref();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
181
330
|
if (_options.log) console.warn('[securenow] Firewall: initial sync failed:', err.message);
|
|
182
331
|
if (_options.failMode === 'closed') {
|
|
183
332
|
_matcher = { isBlocked: () => true, stats: () => ({ exact: 0, cidr: 0, total: 0 }) };
|
|
@@ -193,11 +342,27 @@ function startSyncLoop() {
|
|
|
193
342
|
});
|
|
194
343
|
}
|
|
195
344
|
|
|
345
|
+
function initialAllowlistSync() {
|
|
346
|
+
syncAllowlist((err, changed, stats) => {
|
|
347
|
+
if (err) {
|
|
348
|
+
if (_options.log) console.warn('[securenow] Firewall: initial allowlist sync failed:', err.message);
|
|
349
|
+
const retryTimer = setTimeout(initialAllowlistSync, RETRY_DELAY);
|
|
350
|
+
if (retryTimer.unref) retryTimer.unref();
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (changed && stats && stats.total > 0) {
|
|
354
|
+
if (_options.log) console.log('[securenow] Firewall: synced %d allowed IPs (%d exact + %d CIDR ranges)', stats.total, stats.exact, stats.cidr);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
196
359
|
initialSync();
|
|
360
|
+
initialAllowlistSync();
|
|
197
361
|
|
|
198
362
|
scheduleNextVersionCheck();
|
|
363
|
+
scheduleNextAllowlistVersionCheck();
|
|
199
364
|
|
|
200
|
-
_syncTimer = setInterval(() => { doFullSync(); }, fullSyncIntervalMs);
|
|
365
|
+
_syncTimer = setInterval(() => { doFullSync(); doFullAllowlistSync(); }, fullSyncIntervalMs);
|
|
201
366
|
if (_syncTimer.unref) _syncTimer.unref();
|
|
202
367
|
}
|
|
203
368
|
|
|
@@ -255,24 +420,41 @@ function wrapListener(originalListener) {
|
|
|
255
420
|
};
|
|
256
421
|
}
|
|
257
422
|
|
|
423
|
+
function sendBlockResponse(req, res, ip) {
|
|
424
|
+
const code = (_options && _options.statusCode) || 403;
|
|
425
|
+
const accept = req.headers['accept'] || '';
|
|
426
|
+
if (accept.includes('text/html')) {
|
|
427
|
+
res.writeHead(code, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
428
|
+
res.end(blockedHtml(ip));
|
|
429
|
+
} else {
|
|
430
|
+
res.writeHead(code, { 'Content-Type': 'application/json' });
|
|
431
|
+
res.end(JSON.stringify({ error: 'Forbidden', ip }));
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
258
435
|
function firewallRequestHandler(req, res) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
436
|
+
const ip = resolveClientIp(req);
|
|
437
|
+
|
|
438
|
+
// Allowlist check: if active, only listed IPs are allowed through
|
|
439
|
+
if (_allowlistMatcher && _allowlistMatcher.stats().total > 0) {
|
|
440
|
+
if (!_allowlistMatcher.isBlocked(ip)) {
|
|
262
441
|
_stats.blocked++;
|
|
263
|
-
if (_options && _options.log) console.log('[securenow] Firewall: blocked %s via HTTP', ip);
|
|
264
|
-
|
|
265
|
-
const accept = req.headers['accept'] || '';
|
|
266
|
-
if (accept.includes('text/html')) {
|
|
267
|
-
res.writeHead(code, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
268
|
-
res.end(blockedHtml(ip));
|
|
269
|
-
} else {
|
|
270
|
-
res.writeHead(code, { 'Content-Type': 'application/json' });
|
|
271
|
-
res.end(JSON.stringify({ error: 'Forbidden', ip }));
|
|
272
|
-
}
|
|
442
|
+
if (_options && _options.log) console.log('[securenow] Firewall: blocked %s via HTTP (not in allowlist)', ip);
|
|
443
|
+
sendBlockResponse(req, res, ip);
|
|
273
444
|
return true;
|
|
274
445
|
}
|
|
446
|
+
// IP is on the allowlist — skip blocklist check, allow through
|
|
447
|
+
return false;
|
|
275
448
|
}
|
|
449
|
+
|
|
450
|
+
// Blocklist check
|
|
451
|
+
if (_matcher && _matcher.isBlocked(ip)) {
|
|
452
|
+
_stats.blocked++;
|
|
453
|
+
if (_options && _options.log) console.log('[securenow] Firewall: blocked %s via HTTP', ip);
|
|
454
|
+
sendBlockResponse(req, res, ip);
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
|
|
276
458
|
return false;
|
|
277
459
|
}
|
|
278
460
|
|
|
@@ -330,7 +512,7 @@ function init(options) {
|
|
|
330
512
|
if (_options.tcp) {
|
|
331
513
|
try {
|
|
332
514
|
const tcpLayer = require('./firewall-tcp');
|
|
333
|
-
tcpLayer.init(() => _matcher, _options);
|
|
515
|
+
tcpLayer.init(() => _matcher, _options, () => _allowlistMatcher);
|
|
334
516
|
_layers.push(tcpLayer);
|
|
335
517
|
if (_options.log) console.log('[securenow] Firewall: Layer 2 (TCP drop) active');
|
|
336
518
|
} catch (e) {
|
|
@@ -373,6 +555,7 @@ function init(options) {
|
|
|
373
555
|
|
|
374
556
|
function shutdown() {
|
|
375
557
|
if (_versionTimer) { clearTimeout(_versionTimer); _versionTimer = null; }
|
|
558
|
+
if (_allowlistVersionTimer) { clearTimeout(_allowlistVersionTimer); _allowlistVersionTimer = null; }
|
|
376
559
|
if (_syncTimer) { clearInterval(_syncTimer); _syncTimer = null; }
|
|
377
560
|
|
|
378
561
|
for (const layer of _layers) {
|
|
@@ -392,9 +575,15 @@ function shutdown() {
|
|
|
392
575
|
}
|
|
393
576
|
|
|
394
577
|
function getStats() {
|
|
395
|
-
return {
|
|
578
|
+
return {
|
|
579
|
+
..._stats,
|
|
580
|
+
matcher: _matcher ? _matcher.stats() : null,
|
|
581
|
+
allowlistMatcher: _allowlistMatcher ? _allowlistMatcher.stats() : null,
|
|
582
|
+
initialized: _initialized,
|
|
583
|
+
};
|
|
396
584
|
}
|
|
397
585
|
|
|
398
586
|
function getMatcher() { return _matcher; }
|
|
587
|
+
function getAllowlistMatcher() { return _allowlistMatcher; }
|
|
399
588
|
|
|
400
|
-
module.exports = { init, shutdown, getStats, getMatcher };
|
|
589
|
+
module.exports = { init, shutdown, getStats, getMatcher, getAllowlistMatcher };
|
package/nextjs-webpack-config.js
CHANGED
|
@@ -1,33 +1,100 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Next.js configuration helpers for SecureNow
|
|
5
|
+
*
|
|
6
|
+
* Usage (recommended — zero-list approach):
|
|
7
|
+
*
|
|
8
|
+
* const { withSecureNow } = require('securenow/nextjs-webpack-config');
|
|
9
|
+
* module.exports = withSecureNow({
|
|
10
|
+
* // your existing next.config options
|
|
11
|
+
* });
|
|
12
|
+
*
|
|
13
|
+
* Legacy webpack-only helper (still exported for backwards compat):
|
|
14
|
+
*
|
|
15
|
+
* const { getSecureNowWebpackConfig } = require('securenow/nextjs-webpack-config');
|
|
16
|
+
* module.exports = { webpack: (config, opts) => getSecureNowWebpackConfig(config, opts) };
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const EXTERNAL_PACKAGES = [
|
|
20
|
+
'securenow',
|
|
21
|
+
'@opentelemetry/sdk-node',
|
|
22
|
+
'@opentelemetry/auto-instrumentations-node',
|
|
23
|
+
'@opentelemetry/instrumentation-http',
|
|
24
|
+
'@opentelemetry/exporter-trace-otlp-http',
|
|
25
|
+
'@opentelemetry/exporter-logs-otlp-http',
|
|
26
|
+
'@opentelemetry/sdk-logs',
|
|
27
|
+
'@opentelemetry/instrumentation',
|
|
28
|
+
'@opentelemetry/resources',
|
|
29
|
+
'@opentelemetry/semantic-conventions',
|
|
30
|
+
'@opentelemetry/api',
|
|
31
|
+
'@opentelemetry/api-logs',
|
|
32
|
+
'@vercel/otel',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function detectNextMajor() {
|
|
36
|
+
try {
|
|
37
|
+
const pkg = require('next/package.json');
|
|
38
|
+
return parseInt(pkg.version, 10) || 14;
|
|
39
|
+
} catch {
|
|
40
|
+
return 14;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
1
44
|
/**
|
|
2
|
-
* Next.js
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Usage:
|
|
7
|
-
* const { getSecureNowWebpackConfig } = require('securenow/nextjs-webpack-config');
|
|
8
|
-
*
|
|
9
|
-
* module.exports = {
|
|
10
|
-
* webpack: (config, options) => {
|
|
11
|
-
* return getSecureNowWebpackConfig(config, options);
|
|
12
|
-
* }
|
|
13
|
-
* };
|
|
45
|
+
* Wrap a Next.js config object to auto-externalize SecureNow + OTel
|
|
46
|
+
* packages and enable the instrumentation hook.
|
|
47
|
+
*
|
|
48
|
+
* module.exports = withSecureNow({ reactStrictMode: true });
|
|
14
49
|
*/
|
|
50
|
+
function withSecureNow(userConfig) {
|
|
51
|
+
if (typeof userConfig === 'function') {
|
|
52
|
+
return (...args) => withSecureNow(userConfig(...args));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const cfg = { ...userConfig };
|
|
56
|
+
const major = detectNextMajor();
|
|
57
|
+
|
|
58
|
+
if (major >= 15) {
|
|
59
|
+
cfg.serverExternalPackages = dedup([
|
|
60
|
+
...(cfg.serverExternalPackages || []),
|
|
61
|
+
...EXTERNAL_PACKAGES,
|
|
62
|
+
]);
|
|
63
|
+
} else {
|
|
64
|
+
cfg.experimental = { ...(cfg.experimental || {}) };
|
|
65
|
+
cfg.experimental.instrumentationHook = true;
|
|
66
|
+
cfg.experimental.serverComponentsExternalPackages = dedup([
|
|
67
|
+
...(cfg.experimental.serverComponentsExternalPackages || []),
|
|
68
|
+
...EXTERNAL_PACKAGES,
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const origWebpack = cfg.webpack;
|
|
73
|
+
cfg.webpack = (config, options) => {
|
|
74
|
+
const c = origWebpack ? origWebpack(config, options) : config;
|
|
75
|
+
return getSecureNowWebpackConfig(c, options);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return cfg;
|
|
79
|
+
}
|
|
15
80
|
|
|
81
|
+
function dedup(arr) {
|
|
82
|
+
return [...new Set(arr)];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Legacy: suppress OTel webpack warnings and add externals.
|
|
87
|
+
*/
|
|
16
88
|
function getSecureNowWebpackConfig(config, options) {
|
|
17
89
|
const { isServer } = options;
|
|
18
|
-
|
|
19
|
-
// Only apply to server-side builds
|
|
90
|
+
|
|
20
91
|
if (isServer) {
|
|
21
|
-
// Suppress warnings for OpenTelemetry instrumentations
|
|
22
92
|
config.ignoreWarnings = config.ignoreWarnings || [];
|
|
23
|
-
|
|
24
93
|
config.ignoreWarnings.push(
|
|
25
|
-
// Ignore "Critical dependency" warnings from instrumentations
|
|
26
94
|
{
|
|
27
95
|
module: /@opentelemetry\/instrumentation/,
|
|
28
96
|
message: /Critical dependency: the request of a dependency is an expression/,
|
|
29
97
|
},
|
|
30
|
-
// Ignore missing optional peer dependencies
|
|
31
98
|
{
|
|
32
99
|
module: /@opentelemetry/,
|
|
33
100
|
message: /Module not found.*@opentelemetry\/winston-transport/,
|
|
@@ -35,43 +102,11 @@ function getSecureNowWebpackConfig(config, options) {
|
|
|
35
102
|
{
|
|
36
103
|
module: /@opentelemetry/,
|
|
37
104
|
message: /Module not found.*@opentelemetry\/exporter-jaeger/,
|
|
38
|
-
}
|
|
105
|
+
},
|
|
39
106
|
);
|
|
40
|
-
|
|
41
|
-
// Externalize problematic packages (don't bundle them)
|
|
42
|
-
config.externals = config.externals || [];
|
|
43
|
-
|
|
44
|
-
// Add OpenTelemetry packages as externals
|
|
45
|
-
if (typeof config.externals === 'function') {
|
|
46
|
-
const originalExternals = config.externals;
|
|
47
|
-
config.externals = async (...args) => {
|
|
48
|
-
const result = await originalExternals(...args);
|
|
49
|
-
if (result) return result;
|
|
50
|
-
|
|
51
|
-
const [context, request] = args;
|
|
52
|
-
|
|
53
|
-
// Externalize OpenTelemetry instrumentation packages
|
|
54
|
-
if (request.startsWith('@opentelemetry/')) {
|
|
55
|
-
return `commonjs ${request}`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return undefined;
|
|
59
|
-
};
|
|
60
|
-
} else if (Array.isArray(config.externals)) {
|
|
61
|
-
config.externals.push(/@opentelemetry\//);
|
|
62
|
-
} else {
|
|
63
|
-
config.externals = [/@opentelemetry\//];
|
|
64
|
-
}
|
|
65
107
|
}
|
|
66
|
-
|
|
108
|
+
|
|
67
109
|
return config;
|
|
68
110
|
}
|
|
69
111
|
|
|
70
|
-
module.exports = { getSecureNowWebpackConfig };
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
112
|
+
module.exports = { withSecureNow, getSecureNowWebpackConfig, EXTERNAL_PACKAGES };
|
package/nextjs.js
CHANGED
|
@@ -541,28 +541,6 @@ function registerSecureNow(options = {}) {
|
|
|
541
541
|
}
|
|
542
542
|
} catch (_) {}
|
|
543
543
|
|
|
544
|
-
// Firewall — auto-activates when SECURENOW_API_KEY is set
|
|
545
|
-
const firewallApiKey = env('SECURENOW_API_KEY');
|
|
546
|
-
if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
|
|
547
|
-
try {
|
|
548
|
-
require('./firewall').init({
|
|
549
|
-
apiKey: firewallApiKey,
|
|
550
|
-
apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
|
|
551
|
-
versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
|
|
552
|
-
syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
|
|
553
|
-
failMode: env('SECURENOW_FIREWALL_FAIL_MODE') || 'open',
|
|
554
|
-
statusCode: parseInt(env('SECURENOW_FIREWALL_STATUS_CODE'), 10) || 403,
|
|
555
|
-
log: env('SECURENOW_FIREWALL_LOG') !== '0',
|
|
556
|
-
tcp: env('SECURENOW_FIREWALL_TCP') === '1',
|
|
557
|
-
iptables: env('SECURENOW_FIREWALL_IPTABLES') === '1',
|
|
558
|
-
cloud: env('SECURENOW_FIREWALL_CLOUD') || null,
|
|
559
|
-
});
|
|
560
|
-
} catch (e) {
|
|
561
|
-
console.warn('[securenow] Firewall init failed:', e.message);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
|
|
566
544
|
console.log('[securenow] ✅ OpenTelemetry started for Next.js → %s', tracesUrl);
|
|
567
545
|
console.log('[securenow] 📊 Auto-capturing comprehensive request metadata:');
|
|
568
546
|
console.log('[securenow] • IP addresses (x-forwarded-for, x-real-ip, socket)');
|
|
@@ -594,6 +572,27 @@ function registerSecureNow(options = {}) {
|
|
|
594
572
|
console.error('[securenow] Make sure OpenTelemetry dependencies are installed');
|
|
595
573
|
}
|
|
596
574
|
}
|
|
575
|
+
|
|
576
|
+
// Firewall — runs independently from OTel so it works even if tracing fails
|
|
577
|
+
const firewallApiKey = env('SECURENOW_API_KEY');
|
|
578
|
+
if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
|
|
579
|
+
try {
|
|
580
|
+
require('./firewall').init({
|
|
581
|
+
apiKey: firewallApiKey,
|
|
582
|
+
apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
|
|
583
|
+
versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
|
|
584
|
+
syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
|
|
585
|
+
failMode: env('SECURENOW_FIREWALL_FAIL_MODE') || 'open',
|
|
586
|
+
statusCode: parseInt(env('SECURENOW_FIREWALL_STATUS_CODE'), 10) || 403,
|
|
587
|
+
log: env('SECURENOW_FIREWALL_LOG') !== '0',
|
|
588
|
+
tcp: env('SECURENOW_FIREWALL_TCP') === '1',
|
|
589
|
+
iptables: env('SECURENOW_FIREWALL_IPTABLES') === '1',
|
|
590
|
+
cloud: env('SECURENOW_FIREWALL_CLOUD') || null,
|
|
591
|
+
});
|
|
592
|
+
} catch (e) {
|
|
593
|
+
console.warn('[securenow] Firewall init failed:', e.message);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
597
596
|
}
|
|
598
597
|
|
|
599
598
|
module.exports = {
|
package/nuxt-server-plugin.mjs
CHANGED
|
@@ -324,11 +324,34 @@ export default defineNitroPlugin((nitroApp) => {
|
|
|
324
324
|
// not critical
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
+
// ── Firewall — runs independently from OTel ──
|
|
328
|
+
const firewallApiKey = env('SECURENOW_API_KEY');
|
|
329
|
+
if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
|
|
330
|
+
try {
|
|
331
|
+
const { init: fwInit } = await import('./firewall.js');
|
|
332
|
+
fwInit({
|
|
333
|
+
apiKey: firewallApiKey,
|
|
334
|
+
apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
|
|
335
|
+
versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
|
|
336
|
+
syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
|
|
337
|
+
failMode: env('SECURENOW_FIREWALL_FAIL_MODE') || 'open',
|
|
338
|
+
statusCode: parseInt(env('SECURENOW_FIREWALL_STATUS_CODE'), 10) || 403,
|
|
339
|
+
log: env('SECURENOW_FIREWALL_LOG') !== '0',
|
|
340
|
+
tcp: env('SECURENOW_FIREWALL_TCP') === '1',
|
|
341
|
+
iptables: env('SECURENOW_FIREWALL_IPTABLES') === '1',
|
|
342
|
+
cloud: env('SECURENOW_FIREWALL_CLOUD') || null,
|
|
343
|
+
});
|
|
344
|
+
} catch (e) {
|
|
345
|
+
console.warn('[securenow] Firewall init failed:', e.message);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
327
349
|
// ── Graceful shutdown ──
|
|
328
350
|
const shutdown = async (sig) => {
|
|
329
351
|
try {
|
|
330
352
|
await sdk.shutdown?.();
|
|
331
353
|
if (loggerProvider) await loggerProvider.shutdown?.();
|
|
354
|
+
try { const fw = await import('./firewall.js'); fw.shutdown?.(); } catch {}
|
|
332
355
|
console.log(`[securenow] Shut down on ${sig}`);
|
|
333
356
|
} catch {
|
|
334
357
|
// swallow
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "securenow",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.14.0",
|
|
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",
|
|
@@ -81,6 +81,9 @@
|
|
|
81
81
|
"./firewall": {
|
|
82
82
|
"default": "./firewall.js"
|
|
83
83
|
},
|
|
84
|
+
"./firewall-only": {
|
|
85
|
+
"default": "./firewall-only.js"
|
|
86
|
+
},
|
|
84
87
|
"./cidr": {
|
|
85
88
|
"default": "./cidr.js"
|
|
86
89
|
},
|
|
@@ -117,6 +120,7 @@
|
|
|
117
120
|
"resolve-ip.js",
|
|
118
121
|
"cidr.js",
|
|
119
122
|
"firewall.js",
|
|
123
|
+
"firewall-only.js",
|
|
120
124
|
"firewall-tcp.js",
|
|
121
125
|
"firewall-iptables.js",
|
|
122
126
|
"firewall-cloud.js",
|