securenow 6.0.1 → 6.1.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/CONSUMING-APPS-GUIDE.md +455 -0
- package/NPM_README.md +2029 -0
- package/README.md +297 -40
- package/SKILL-API.md +634 -0
- package/SKILL-CLI.md +454 -0
- package/cidr.js +83 -0
- package/cli/apps.js +585 -0
- package/cli/auth.js +280 -0
- package/cli/client.js +115 -0
- package/cli/config.js +173 -0
- package/cli/diagnostics.js +387 -0
- package/cli/firewall.js +100 -0
- package/cli/fp.js +638 -0
- package/cli/init.js +201 -0
- package/cli/monitor.js +440 -0
- package/cli/run.js +148 -0
- package/cli/security.js +980 -0
- package/cli/ui.js +386 -0
- package/cli/utils.js +127 -0
- package/cli.js +466 -455
- package/console-instrumentation.js +147 -136
- package/docs/ALL-FRAMEWORKS-QUICKSTART.md +1377 -455
- package/docs/API-KEYS-GUIDE.md +233 -0
- package/docs/ARCHITECTURE.md +3 -3
- package/docs/AUTO-BODY-CAPTURE.md +1 -1
- package/docs/AUTO-SETUP-SUMMARY.md +331 -0
- package/docs/AUTO-SETUP.md +4 -4
- package/docs/AUTOMATIC-IP-CAPTURE.md +5 -5
- package/docs/BODY-CAPTURE-FIX.md +261 -0
- package/docs/BODY-CAPTURE-QUICKSTART.md +2 -2
- package/docs/CHANGELOG-NEXTJS.md +1 -35
- package/docs/COMPLETION-REPORT.md +408 -0
- package/docs/CUSTOMER-GUIDE.md +16 -16
- package/docs/EASIEST-SETUP.md +5 -5
- package/docs/ENVIRONMENT-VARIABLES.md +880 -652
- package/docs/EXPRESS-BODY-CAPTURE.md +13 -12
- package/docs/EXPRESS-SETUP-GUIDE.md +719 -720
- package/docs/FINAL-SOLUTION.md +335 -0
- package/docs/FIREWALL-GUIDE.md +426 -0
- package/docs/IMPLEMENTATION-SUMMARY.md +410 -0
- package/docs/INDEX.md +22 -4
- package/docs/LOGGING-GUIDE.md +701 -708
- package/docs/LOGGING-QUICKSTART.md +234 -255
- package/docs/NEXTJS-BODY-CAPTURE-COMPARISON.md +323 -0
- package/docs/NEXTJS-BODY-CAPTURE.md +2 -2
- package/docs/NEXTJS-GUIDE.md +14 -14
- package/docs/NEXTJS-QUICKSTART.md +1 -1
- package/docs/NEXTJS-SETUP-COMPLETE.md +795 -0
- package/docs/NEXTJS-WRAPPER-APPROACH.md +1 -1
- package/docs/NUXT-GUIDE.md +166 -0
- package/docs/QUICKSTART-BODY-CAPTURE.md +2 -2
- package/docs/REDACTION-EXAMPLES.md +1 -1
- package/docs/REQUEST-BODY-CAPTURE.md +19 -10
- package/docs/SOLUTION-SUMMARY.md +312 -0
- package/docs/VERCEL-OTEL-MIGRATION.md +3 -3
- package/examples/README.md +6 -6
- package/examples/instrumentation-with-auto-capture.ts +1 -1
- package/examples/nextjs-env-example.txt +2 -2
- package/examples/nextjs-instrumentation.js +1 -1
- package/examples/nextjs-instrumentation.ts +1 -1
- package/examples/nextjs-with-logging-example.md +6 -6
- package/examples/nextjs-with-options.ts +1 -1
- package/examples/test-nextjs-setup.js +1 -1
- package/firewall-cloud.js +212 -0
- package/firewall-iptables.js +139 -0
- package/firewall-only.js +38 -0
- package/firewall-tcp.js +74 -0
- package/firewall.js +720 -0
- package/free-trial-banner.js +174 -0
- package/nextjs-auto-capture.js +199 -207
- package/nextjs-middleware.js +186 -181
- package/nextjs-webpack-config.js +88 -53
- package/nextjs-wrapper.js +158 -158
- package/nextjs.d.ts +1 -1
- package/nextjs.js +224 -198
- package/nuxt-server-plugin.mjs +423 -0
- package/nuxt.d.ts +60 -0
- package/nuxt.mjs +75 -0
- package/package.json +67 -45
- package/postinstall.js +6 -6
- package/register.d.ts +1 -1
- package/register.js +39 -4
- package/resolve-ip.js +77 -0
- package/tracing.d.ts +2 -1
- package/tracing.js +333 -31
- package/web-vite.mjs +239 -156
- package/LICENSE +0 -15
|
@@ -21,7 +21,7 @@ export function register() {
|
|
|
21
21
|
* SECURENOW_APPID=my-nextjs-app
|
|
22
22
|
*
|
|
23
23
|
* Optional:
|
|
24
|
-
* SECURENOW_INSTANCE=http://your-
|
|
24
|
+
* SECURENOW_INSTANCE=http://your-otlp-collector:4318
|
|
25
25
|
* SECURENOW_NO_UUID=1 # Don't append UUID to service name
|
|
26
26
|
* OTEL_LOG_LEVEL=info # debug|info|warn|error
|
|
27
27
|
* SECURENOW_DISABLE_INSTRUMENTATIONS=fs # Comma-separated list
|
|
@@ -21,7 +21,7 @@ export function register() {
|
|
|
21
21
|
* SECURENOW_APPID=my-nextjs-app
|
|
22
22
|
*
|
|
23
23
|
* Optional:
|
|
24
|
-
* SECURENOW_INSTANCE=http://your-
|
|
24
|
+
* SECURENOW_INSTANCE=http://your-otlp-collector:4318
|
|
25
25
|
* SECURENOW_NO_UUID=1 # Don't append UUID to service name
|
|
26
26
|
* OTEL_LOG_LEVEL=info # debug|info|warn|error
|
|
27
27
|
* SECURENOW_DISABLE_INSTRUMENTATIONS=fs # Comma-separated list
|
|
@@ -39,9 +39,9 @@ SECURENOW_LOGGING_ENABLED=1
|
|
|
39
39
|
SECURENOW_APPID=my-nextjs-app
|
|
40
40
|
SECURENOW_INSTANCE=http://localhost:4318
|
|
41
41
|
|
|
42
|
-
# For
|
|
43
|
-
# SECURENOW_INSTANCE=https://ingest.<region>.
|
|
44
|
-
# OTEL_EXPORTER_OTLP_HEADERS="
|
|
42
|
+
# For SecureNow or managed OTLP (example):
|
|
43
|
+
# SECURENOW_INSTANCE=https://ingest.<region>.securenow.ai:443
|
|
44
|
+
# OTEL_EXPORTER_OTLP_HEADERS="x-api-key=<your-key>"
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
### 4. Enable Instrumentation in `next.config.js`
|
|
@@ -239,9 +239,9 @@ You should see in the console:
|
|
|
239
239
|
SecureNow initialized with logging enabled
|
|
240
240
|
```
|
|
241
241
|
|
|
242
|
-
## View Logs in
|
|
242
|
+
## View Logs in SecureNow
|
|
243
243
|
|
|
244
|
-
1. Open your
|
|
244
|
+
1. Open your SecureNow dashboard
|
|
245
245
|
2. Navigate to **Logs** section
|
|
246
246
|
3. Filter by `service.name = my-nextjs-app`
|
|
247
247
|
4. See all your application logs with:
|
|
@@ -298,4 +298,4 @@ await import('securenow/console-instrumentation'); // Second
|
|
|
298
298
|
|
|
299
299
|
- [Complete Logging Guide](../docs/LOGGING-GUIDE.md)
|
|
300
300
|
- [Next.js Complete Guide](../docs/NEXTJS-GUIDE.md)
|
|
301
|
-
- [
|
|
301
|
+
- [SecureNow](https://securenow.ai/)
|
|
@@ -10,7 +10,7 @@ import { registerSecureNow } from 'securenow/nextjs';
|
|
|
10
10
|
export function register() {
|
|
11
11
|
registerSecureNow({
|
|
12
12
|
serviceName: 'my-nextjs-app',
|
|
13
|
-
endpoint: 'http://your-
|
|
13
|
+
endpoint: 'http://your-otlp-collector:4318',
|
|
14
14
|
noUuid: false,
|
|
15
15
|
disableInstrumentations: ['fs', 'dns'],
|
|
16
16
|
headers: {
|
|
@@ -58,7 +58,7 @@ setTimeout(() => {
|
|
|
58
58
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
59
59
|
console.log('✅ All tests passed!');
|
|
60
60
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
61
|
-
console.log('\nCheck your
|
|
61
|
+
console.log('\nCheck your SecureNow dashboard for traces from "test-nextjs-app"\n');
|
|
62
62
|
process.exit(0);
|
|
63
63
|
}, 2000);
|
|
64
64
|
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Layer 4: Cloud/Edge WAF integration.
|
|
5
|
+
* Pushes the blocklist to the customer's cloud WAF provider so traffic is
|
|
6
|
+
* blocked at the CDN/edge before it ever reaches the origin server.
|
|
7
|
+
*
|
|
8
|
+
* Supported providers: cloudflare, aws, gcp
|
|
9
|
+
* Cloud SDKs are optional peer dependencies — only required when enabled.
|
|
10
|
+
* Cloud credentials are never logged.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const https = require('https');
|
|
14
|
+
|
|
15
|
+
let _options = null;
|
|
16
|
+
let _active = false;
|
|
17
|
+
let _provider = null;
|
|
18
|
+
let _lastPushTime = 0;
|
|
19
|
+
|
|
20
|
+
const MIN_PUSH_INTERVAL_MS = 30000;
|
|
21
|
+
|
|
22
|
+
// ────── Cloudflare ──────
|
|
23
|
+
|
|
24
|
+
async function cloudflareSync(ips) {
|
|
25
|
+
const token = process.env.CLOUDFLARE_API_TOKEN;
|
|
26
|
+
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
|
|
27
|
+
if (!token || !accountId) throw new Error('Missing CLOUDFLARE_API_TOKEN or CLOUDFLARE_ACCOUNT_ID');
|
|
28
|
+
|
|
29
|
+
const listName = 'securenow-blocklist';
|
|
30
|
+
|
|
31
|
+
const lists = await cfApi('GET', `/accounts/${accountId}/rules/lists`, token);
|
|
32
|
+
let list = lists.result?.find(l => l.name === listName);
|
|
33
|
+
|
|
34
|
+
if (!list) {
|
|
35
|
+
const created = await cfApi('POST', `/accounts/${accountId}/rules/lists`, token, {
|
|
36
|
+
name: listName,
|
|
37
|
+
kind: 'ip',
|
|
38
|
+
description: 'Managed by SecureNow firewall SDK',
|
|
39
|
+
});
|
|
40
|
+
list = created.result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const items = ips.map(ip => ({ ip: ip.includes('/') ? ip : ip + '/32' }));
|
|
44
|
+
|
|
45
|
+
await cfApi('PUT', `/accounts/${accountId}/rules/lists/${list.id}/items`, token, items);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function cfApi(method, path, token, body) {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const data = body ? JSON.stringify(body) : null;
|
|
51
|
+
const req = https.request({
|
|
52
|
+
hostname: 'api.cloudflare.com',
|
|
53
|
+
path: '/client/v4' + path,
|
|
54
|
+
method,
|
|
55
|
+
headers: {
|
|
56
|
+
'Authorization': `Bearer ${token}`,
|
|
57
|
+
'Content-Type': 'application/json',
|
|
58
|
+
...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}),
|
|
59
|
+
},
|
|
60
|
+
timeout: 15000,
|
|
61
|
+
}, (res) => {
|
|
62
|
+
let chunks = '';
|
|
63
|
+
res.on('data', c => chunks += c);
|
|
64
|
+
res.on('end', () => {
|
|
65
|
+
try { resolve(JSON.parse(chunks)); } catch (e) { reject(new Error(`CF parse error: ${chunks.slice(0, 200)}`)); }
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
req.on('error', reject);
|
|
69
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Cloudflare API timeout')); });
|
|
70
|
+
if (data) req.write(data);
|
|
71
|
+
req.end();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ────── AWS WAF ──────
|
|
76
|
+
|
|
77
|
+
async function awsSync(ips) {
|
|
78
|
+
let WAFV2;
|
|
79
|
+
try {
|
|
80
|
+
WAFV2 = require('@aws-sdk/client-wafv2');
|
|
81
|
+
} catch {
|
|
82
|
+
throw new Error('AWS WAF requires @aws-sdk/client-wafv2 — install it: npm i @aws-sdk/client-wafv2');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const ipSetId = process.env.AWS_WAF_IP_SET_ID;
|
|
86
|
+
const ipSetName = process.env.AWS_WAF_IP_SET_NAME || 'securenow-blocklist';
|
|
87
|
+
const scope = process.env.AWS_WAF_SCOPE || 'REGIONAL';
|
|
88
|
+
if (!ipSetId) throw new Error('Missing AWS_WAF_IP_SET_ID');
|
|
89
|
+
|
|
90
|
+
const client = new WAFV2.WAFV2Client({});
|
|
91
|
+
|
|
92
|
+
const { IPSet, LockToken } = await client.send(new WAFV2.GetIPSetCommand({
|
|
93
|
+
Name: ipSetName,
|
|
94
|
+
Scope: scope,
|
|
95
|
+
Id: ipSetId,
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
const addresses = ips.map(ip => ip.includes('/') ? ip : ip + '/32');
|
|
99
|
+
|
|
100
|
+
await client.send(new WAFV2.UpdateIPSetCommand({
|
|
101
|
+
Name: ipSetName,
|
|
102
|
+
Scope: scope,
|
|
103
|
+
Id: ipSetId,
|
|
104
|
+
Addresses: addresses,
|
|
105
|
+
LockToken,
|
|
106
|
+
Description: 'Managed by SecureNow firewall SDK',
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ────── GCP Cloud Armor ──────
|
|
111
|
+
|
|
112
|
+
async function gcpSync(ips) {
|
|
113
|
+
let compute;
|
|
114
|
+
try {
|
|
115
|
+
compute = require('@google-cloud/compute');
|
|
116
|
+
} catch {
|
|
117
|
+
throw new Error('GCP Cloud Armor requires @google-cloud/compute — install it: npm i @google-cloud/compute');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const project = process.env.GCP_PROJECT_ID;
|
|
121
|
+
const policyName = process.env.GCP_SECURITY_POLICY;
|
|
122
|
+
if (!project || !policyName) throw new Error('Missing GCP_PROJECT_ID or GCP_SECURITY_POLICY');
|
|
123
|
+
|
|
124
|
+
const client = new compute.SecurityPoliciesClient();
|
|
125
|
+
const rulePriority = 2000;
|
|
126
|
+
|
|
127
|
+
const srcRanges = ips.map(ip => ip.includes('/') ? ip : ip + '/32');
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
await client.patchRule({
|
|
131
|
+
project,
|
|
132
|
+
securityPolicy: policyName,
|
|
133
|
+
priority: String(rulePriority),
|
|
134
|
+
securityPolicyRuleResource: {
|
|
135
|
+
priority: rulePriority,
|
|
136
|
+
action: 'deny(403)',
|
|
137
|
+
match: { versionedExpr: 'SRC_IPS_V1', config: { srcIpRanges: srcRanges } },
|
|
138
|
+
description: 'Managed by SecureNow firewall SDK',
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
} catch (e) {
|
|
142
|
+
if (e.code === 404 || e.message?.includes('not found')) {
|
|
143
|
+
await client.addRule({
|
|
144
|
+
project,
|
|
145
|
+
securityPolicy: policyName,
|
|
146
|
+
securityPolicyRuleResource: {
|
|
147
|
+
priority: rulePriority,
|
|
148
|
+
action: 'deny(403)',
|
|
149
|
+
match: { versionedExpr: 'SRC_IPS_V1', config: { srcIpRanges: srcRanges } },
|
|
150
|
+
description: 'Managed by SecureNow firewall SDK',
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
} else {
|
|
154
|
+
throw e;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ────── Provider dispatch ──────
|
|
160
|
+
|
|
161
|
+
const PROVIDERS = {
|
|
162
|
+
cloudflare: cloudflareSync,
|
|
163
|
+
aws: awsSync,
|
|
164
|
+
gcp: gcpSync,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// ────── Public API ──────
|
|
168
|
+
|
|
169
|
+
function init(options) {
|
|
170
|
+
_options = options;
|
|
171
|
+
const provider = (options.cloud || '').toLowerCase();
|
|
172
|
+
|
|
173
|
+
if (!PROVIDERS[provider]) {
|
|
174
|
+
if (_options.log) console.warn('[securenow] Firewall cloud: unknown provider "%s", expected cloudflare|aws|gcp', provider);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
_provider = provider;
|
|
179
|
+
_active = true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Called by the firewall core after each successful blocklist sync.
|
|
184
|
+
* Throttled to max 1 push per MIN_PUSH_INTERVAL_MS.
|
|
185
|
+
* @param {string[]} ips - Array of IPs and CIDRs to push
|
|
186
|
+
*/
|
|
187
|
+
async function sync(ips) {
|
|
188
|
+
if (!_active || !_provider) return;
|
|
189
|
+
|
|
190
|
+
const now = Date.now();
|
|
191
|
+
if (now - _lastPushTime < MIN_PUSH_INTERVAL_MS) return;
|
|
192
|
+
_lastPushTime = now;
|
|
193
|
+
|
|
194
|
+
const dryRun = process.env.SECURENOW_FIREWALL_CLOUD_DRY_RUN === '1';
|
|
195
|
+
if (dryRun) {
|
|
196
|
+
if (_options.log) console.log('[securenow] Firewall cloud: dry-run — would push %d IPs to %s', ips.length, _provider);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
await PROVIDERS[_provider](ips);
|
|
202
|
+
if (_options.log) console.log('[securenow] Firewall cloud: pushed %d IPs to %s', ips.length, _provider);
|
|
203
|
+
} catch (e) {
|
|
204
|
+
if (_options.log) console.warn('[securenow] Firewall cloud: push to %s failed:', _provider, e.message);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function shutdown() {
|
|
209
|
+
_active = false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = { init, sync, shutdown };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Layer 3: OS-level firewall via iptables/nftables.
|
|
5
|
+
* Manages a dedicated SECURENOW_BLOCK chain — never touches customer rules.
|
|
6
|
+
* Linux only, requires root or CAP_NET_ADMIN. Falls back gracefully otherwise.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { execSync } = require('child_process');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
const CHAIN_NAME = 'SECURENOW_BLOCK';
|
|
13
|
+
|
|
14
|
+
let _options = null;
|
|
15
|
+
let _active = false;
|
|
16
|
+
let _useNft = false;
|
|
17
|
+
|
|
18
|
+
function exec(cmd) {
|
|
19
|
+
return execSync(cmd, { stdio: 'pipe', timeout: 10000 }).toString().trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function canRun(cmd) {
|
|
23
|
+
try { exec(cmd); return true; } catch { return false; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function detectBackend() {
|
|
27
|
+
if (canRun('nft --version')) return 'nft';
|
|
28
|
+
if (canRun('iptables --version')) return 'iptables';
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ────── iptables backend ──────
|
|
33
|
+
|
|
34
|
+
function iptablesSetup() {
|
|
35
|
+
try { exec(`iptables -N ${CHAIN_NAME}`); } catch (_) {} // chain may already exist
|
|
36
|
+
try { exec(`iptables -C INPUT -j ${CHAIN_NAME}`); } catch (_) {
|
|
37
|
+
exec(`iptables -I INPUT 1 -j ${CHAIN_NAME}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function iptablesSync(ips) {
|
|
42
|
+
exec(`iptables -F ${CHAIN_NAME}`);
|
|
43
|
+
for (const ip of ips) {
|
|
44
|
+
exec(`iptables -A ${CHAIN_NAME} -s ${ip} -j DROP`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function iptablesCleanup() {
|
|
49
|
+
try { exec(`iptables -D INPUT -j ${CHAIN_NAME}`); } catch (_) {}
|
|
50
|
+
try { exec(`iptables -F ${CHAIN_NAME}`); } catch (_) {}
|
|
51
|
+
try { exec(`iptables -X ${CHAIN_NAME}`); } catch (_) {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ────── nftables backend ──────
|
|
55
|
+
|
|
56
|
+
function nftSetup() {
|
|
57
|
+
try { exec(`nft list chain ip filter ${CHAIN_NAME}`); } catch (_) {
|
|
58
|
+
exec(`nft add chain ip filter ${CHAIN_NAME}`);
|
|
59
|
+
}
|
|
60
|
+
try { exec(`nft insert rule ip filter INPUT jump ${CHAIN_NAME}`); } catch (_) {}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function nftSync(ips) {
|
|
64
|
+
exec(`nft flush chain ip filter ${CHAIN_NAME}`);
|
|
65
|
+
for (const ip of ips) {
|
|
66
|
+
exec(`nft add rule ip filter ${CHAIN_NAME} ip saddr ${ip} drop`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function nftCleanup() {
|
|
71
|
+
try { exec(`nft flush chain ip filter ${CHAIN_NAME}`); } catch (_) {}
|
|
72
|
+
try { exec(`nft delete chain ip filter ${CHAIN_NAME}`); } catch (_) {}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ────── Public API ──────
|
|
76
|
+
|
|
77
|
+
let _syncCallback = null;
|
|
78
|
+
|
|
79
|
+
function init(options) {
|
|
80
|
+
_options = options;
|
|
81
|
+
|
|
82
|
+
if (os.platform() !== 'linux') {
|
|
83
|
+
if (_options.log) console.warn('[securenow] Firewall iptables: only supported on Linux, skipping');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const backend = detectBackend();
|
|
88
|
+
if (!backend) {
|
|
89
|
+
if (_options.log) console.warn('[securenow] Firewall iptables: neither iptables nor nft found, skipping');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_useNft = backend === 'nft';
|
|
94
|
+
const label = _useNft ? 'nftables' : 'iptables';
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
if (_useNft) nftSetup(); else iptablesSetup();
|
|
98
|
+
_active = true;
|
|
99
|
+
if (_options.log) console.log('[securenow] Firewall iptables: %s chain %s ready', label, CHAIN_NAME);
|
|
100
|
+
} catch (e) {
|
|
101
|
+
if (_options.log) console.warn('[securenow] Firewall iptables: setup failed (need root or CAP_NET_ADMIN):', e.message);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Called by the firewall core after each successful blocklist sync.
|
|
107
|
+
* @param {string[]} ips - Array of IPs and CIDRs to block
|
|
108
|
+
*/
|
|
109
|
+
function sync(ips) {
|
|
110
|
+
if (!_active) return;
|
|
111
|
+
|
|
112
|
+
const maxRules = (_options && _options.iptablesMax) || 10000;
|
|
113
|
+
const limited = ips.length > maxRules ? ips.slice(0, maxRules) : ips;
|
|
114
|
+
|
|
115
|
+
if (ips.length > maxRules && _options.log) {
|
|
116
|
+
console.warn('[securenow] Firewall iptables: truncated to %d rules (max %d)', maxRules, maxRules);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
if (_useNft) nftSync(limited); else iptablesSync(limited);
|
|
121
|
+
} catch (e) {
|
|
122
|
+
if (_options && _options.log) {
|
|
123
|
+
console.warn('[securenow] Firewall iptables: sync failed:', e.message);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function shutdown() {
|
|
129
|
+
if (!_active) return;
|
|
130
|
+
try {
|
|
131
|
+
if (_useNft) nftCleanup(); else iptablesCleanup();
|
|
132
|
+
if (_options && _options.log) console.log('[securenow] Firewall iptables: chain %s cleaned up', CHAIN_NAME);
|
|
133
|
+
} catch (e) {
|
|
134
|
+
if (_options && _options.log) console.warn('[securenow] Firewall iptables: cleanup failed:', e.message);
|
|
135
|
+
}
|
|
136
|
+
_active = false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = { init, sync, shutdown };
|
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
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Layer 2: TCP-level blocking via net.Server connection event.
|
|
5
|
+
* Destroys the socket before HTTP parsing starts. Zero bytes sent back.
|
|
6
|
+
*
|
|
7
|
+
* Caveat: only sees socket.remoteAddress (direct connection IP).
|
|
8
|
+
* If behind a reverse proxy, the proxy IP is seen instead.
|
|
9
|
+
* Proxy IPs are skipped (let through to Layer 1 for proper header-based resolution).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const net = require('net');
|
|
13
|
+
const { resolveSocketIp, isFromTrustedProxy } = require('./resolve-ip');
|
|
14
|
+
|
|
15
|
+
let _getMatcher = null;
|
|
16
|
+
let _getAllowlistMatcher = null;
|
|
17
|
+
let _options = null;
|
|
18
|
+
let _patched = false;
|
|
19
|
+
const _origListen = net.Server.prototype.listen;
|
|
20
|
+
|
|
21
|
+
function onConnection(socket) {
|
|
22
|
+
const ip = resolveSocketIp(socket);
|
|
23
|
+
|
|
24
|
+
// Skip if the connection is from a trusted proxy — Layer 1 will handle it
|
|
25
|
+
// with proper X-Forwarded-For resolution
|
|
26
|
+
if (isFromTrustedProxy(ip) || isFromTrustedProxy('::ffff:' + ip)) return;
|
|
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
|
+
|
|
45
|
+
if (matcher.isBlocked(ip)) {
|
|
46
|
+
if (_options && _options.log) {
|
|
47
|
+
console.log('[securenow] Firewall: blocked %s via TCP (socket destroyed)', ip);
|
|
48
|
+
}
|
|
49
|
+
socket.destroy();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function init(getMatcher, options, getAllowlistMatcher) {
|
|
54
|
+
_getMatcher = getMatcher;
|
|
55
|
+
_getAllowlistMatcher = getAllowlistMatcher || null;
|
|
56
|
+
_options = options;
|
|
57
|
+
|
|
58
|
+
if (_patched) return;
|
|
59
|
+
_patched = true;
|
|
60
|
+
|
|
61
|
+
net.Server.prototype.listen = function(...args) {
|
|
62
|
+
this.on('connection', onConnection);
|
|
63
|
+
return _origListen.apply(this, args);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function shutdown() {
|
|
68
|
+
if (_patched) {
|
|
69
|
+
net.Server.prototype.listen = _origListen;
|
|
70
|
+
_patched = false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { init, shutdown };
|