securenow 8.25.0 → 8.25.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NPM_README.md +1 -1
- package/SKILL-API.md +5 -5
- package/firewall-cloud.js +28 -11
- package/package.json +1 -1
package/NPM_README.md
CHANGED
|
@@ -1472,7 +1472,7 @@ The firewall supports four layers -- Layer 1 is always on, the rest are opt-in:
|
|
|
1472
1472
|
| **Layer 1: HTTP** | *(always on)* | Returns 403 (`config.firewall.statusCode`): JSON `{"error":"Forbidden","ip":"..."}` for API clients, a security alert page for browsers. Works with proxy headers. |
|
|
1473
1473
|
| **Layer 2: TCP** | `config.firewall.tcp=true` | `socket.destroy()` before HTTP parsing -- zero bytes sent back. Sees only the connection's source IP; connections from trusted proxies are left to Layer 1. Checked on each server `connection` **(8.25.0+)**; before, it hooked `listen()` and missed the servers most apps start before the SDK has finished starting |
|
|
1474
1474
|
| **Layer 3: iptables** | `config.firewall.iptables=true` | Kernel-level DROP in a dedicated `SECURENOW_BLOCK` chain (ip6tables for IPv6), or a `securenow` nft table where iptables is missing **(8.25.0+)**. Linux, root or `CAP_NET_ADMIN`; direct client connections only. 8.25.0 validates every entry, runs without a shell and applies each sync as one batch |
|
|
1475
|
-
| **Layer 4: Cloud WAF** | `config.firewall.cloud="cloudflare"` / `"aws"` / `"gcp"` | Pushes the global blocklist to Cloudflare (an account IP list named `securenow-blocklist` that your own WAF rule must reference), AWS WAFv2 (IP set; `npm i @aws-sdk/client-wafv2`) or GCP Cloud Armor (`deny(403)` rules at priority 2000; `npm i @google-cloud/compute`), at most every 30 seconds. `config.firewall.cloudDryRun=true` only logs. 8.25.0 fixes IPv6 ranges, Cloud Armor's 10-range limit and stale rules.
|
|
1475
|
+
| **Layer 4: Cloud WAF** | `config.firewall.cloud="cloudflare"` / `"aws"` / `"gcp"` | Pushes the global blocklist to Cloudflare (an account IP list named `securenow-blocklist` that your own WAF rule must reference), AWS WAFv2 (IP set; `npm i @aws-sdk/client-wafv2`) or GCP Cloud Armor (`deny(403)` rules at priority 2000; `npm i @google-cloud/compute`), at most every 30 seconds. `config.firewall.cloudDryRun=true` only logs. 8.25.0 fixes IPv6 ranges, Cloud Armor's 10-range limit and stale rules. AWS tested against real IP sets (8.25.2+); Cloudflare and GCP request format tested only |
|
|
1476
1476
|
|
|
1477
1477
|
Layers 2 to 4 carry the global blocklist; scoped blocks, rate limits and challenges are Layer 1 only. Turn Layers 2 to 4 on with 8.25.0+. In a container, Layer 3 needs `--cap-add NET_ADMIN` and the `iptables` package.
|
|
1478
1478
|
|
package/SKILL-API.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **Node.js security:** Use a supported, patched runtime: Node.js 22.23.4+, 24.22.1+, or 26.12.1+ on its respective release line. Earlier patches are affected by [CVE-2026-111497](https://nodejs.org/en/blog/vulnerability/october-2026-security-releases). Upgrading the SecureNow package does not patch Node.js; upgrade the runtime and restart every application process.
|
|
4
4
|
|
|
5
|
-
The integration playbook for the `securenow` npm package. A coding agent that reads only this file can plug SecureNow into a Node.js app, turn on every feature, deploy it and prove it works. It describes 8.25.
|
|
5
|
+
The integration playbook for the `securenow` npm package. A coding agent that reads only this file can plug SecureNow into a Node.js app, turn on every feature, deploy it and prove it works. It describes 8.25.2; items marked **(8.25.0+)** are new in 8.25.0, so an older install needs `npm install securenow@latest` for them (check with `npx securenow version`). The terminal reference is [SKILL-CLI.md](./SKILL-CLI.md). Both files ship in `node_modules/securenow/`.
|
|
6
6
|
|
|
7
7
|
## Rules for the agent
|
|
8
8
|
|
|
@@ -384,7 +384,7 @@ So a `runtime.json` left on a server beats the `credentials.production.json` nex
|
|
|
384
384
|
| 1 HTTP | always, once the runtime API key is present | 403 JSON `{"error":"Forbidden","ip":"..."}`, or an HTML page with the masked IP for browsers; status from `firewall.statusCode` | Works behind proxies (forwarding headers) |
|
|
385
385
|
| 2 TCP | `"tcp": true` | Destroys the socket before HTTP parsing, zero bytes sent | Sees only the connection's source IP; connections from trusted proxies are left to layer 1. Use 8.25.0+: before, it hooked `listen()` and missed the servers most apps start before the SDK has finished starting |
|
|
386
386
|
| 3 OS | `"iptables": true` | Kernel DROP: a `SECURENOW_BLOCK` chain in iptables (ip6tables for IPv6), or a `securenow` nft table where iptables is missing **(8.25.0+)** | Linux, root or `CAP_NET_ADMIN`, `iptables`/`nft` installed; direct client connections only. Use 8.25.0+: it validates every entry, runs without a shell and applies each sync as one batch, and an IPv6 entry no longer stops the sync |
|
|
387
|
-
| 4 Cloud WAF | `"cloud": "cloudflare"`, `"aws"` or `"gcp"` | Pushes the global blocklist to the edge, at most every 30 s | Cloudflare: fills an account IP list `securenow-blocklist` that your own WAF rule must reference. AWS: WAFv2 IP set. GCP: Cloud Armor rules at priority 2000, `deny(403)`. Use 8.25.
|
|
387
|
+
| 4 Cloud WAF | `"cloud": "cloudflare"`, `"aws"` or `"gcp"` | Pushes the global blocklist to the edge, at most every 30 s | Cloudflare: fills an account IP list `securenow-blocklist` that your own WAF rule must reference. AWS: WAFv2 IP set. GCP: Cloud Armor rules at priority 2000, `deny(403)`. Use 8.25.2+ (IPv6 ranges, Cloud Armor's 10-range limit, stale rules, IPv4-mapped entries fixed). AWS tested against real IP sets; Cloudflare and GCP request format tested only |
|
|
388
388
|
|
|
389
389
|
Layers 2 to 4 carry the global blocklist. Scoped blocks, rate limits and challenges are layer 1 only.
|
|
390
390
|
- **On:** automatic with the runtime API key. Each app and environment also has a remote switch, `npx securenow firewall enable|disable --app <key> --env production`, applied without a deploy. `config.firewall.enabled` is ignored.
|
|
@@ -419,7 +419,7 @@ So a `runtime.json` left on a server beats the `credentials.production.json` nex
|
|
|
419
419
|
|
|
420
420
|
### 5.8 Blocklist, allowlist, trusted IPs
|
|
421
421
|
|
|
422
|
-
- **Blocklist:** block an
|
|
422
|
+
- **Blocklist:** block an IP address or CIDR range, IPv4 or IPv6, everywhere or on a route, optionally with an expiry (IPv6 ranges are enforced by securenow 8.25.0+). An unblock stops enforcement and keeps the history.
|
|
423
423
|
|
|
424
424
|
```bash
|
|
425
425
|
npx securenow blocklist add 203.0.113.42 --env production --duration 24h --reason "credential stuffing"
|
|
@@ -806,8 +806,8 @@ Bad signs: no `[securenow]` line at all (the preload is not on the command that
|
|
|
806
806
|
|
|
807
807
|
**Not supported:** Edge runtimes (Cloudflare Workers, Vercel Edge, Next.js Edge middleware and routes), Deno, `Bun.serve`, browser telemetry.
|
|
808
808
|
**Not tested:** Vercel (firewall unverified on its serverless functions), AWS Lambda and other serverless platforms.
|
|
809
|
-
**Layers 2 to 4:** turn them on with 8.25.0+ only (in 8.24.0 layer 2 dropped nothing for most apps and layers 3 and 4 mishandled IPv6). **Cloud WAF (layer 4):**
|
|
810
|
-
**IPv6:**
|
|
809
|
+
**Layers 2 to 4:** turn them on with 8.25.0+ only (in 8.24.0 layer 2 dropped nothing for most apps and layers 3 and 4 mishandled IPv6). **Cloud WAF (layer 4):** AWS WAF is tested against real IP sets (IPv4 and IPv6, emptied list) from 8.25.2; Cloudflare and GCP are tested on the request format only, not against real accounts. On Cloudflare your own WAF rule must reference the `securenow-blocklist` list.
|
|
810
|
+
**IPv6:** the blocklist, allowlist, trusted IPs, rate limits, challenges and API-key IP limits take IPv6 addresses and CIDR ranges, stored in canonical form (`2001:DB8::1` = `2001:db8::1`); layer 1 and trusted proxies match them **(8.25.0+; older SDKs match exact IPv6 addresses but ignore IPv6 ranges)**. A trusted-IP range can be no wider than /8 (IPv4) or /32 (IPv6).
|
|
811
811
|
**`nuxt dev`:** before 8.25.0 the client IP was unknown, so IP blocking only applied to the built server.
|
|
812
812
|
**Log redaction (8.25.0+)** goes by field names and value shapes: a secret in free text with no recognizable shape is exported as written.
|
|
813
813
|
|
package/firewall-cloud.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
const https = require('https');
|
|
14
|
-
const
|
|
14
|
+
const { parseIp, parseCidr, parseCidr6, intToIp, bigIntToIpv6 } = require('./cidr');
|
|
15
15
|
|
|
16
16
|
let _options = null;
|
|
17
17
|
let _active = false;
|
|
@@ -24,21 +24,38 @@ const GCP_MAX_RULES = 100;
|
|
|
24
24
|
|
|
25
25
|
const MIN_PUSH_INTERVAL_MS = 30000;
|
|
26
26
|
|
|
27
|
-
// Blocklist entries as CIDRs, per address family: an exact address becomes /32 (IPv4) or /128
|
|
28
|
-
// (
|
|
27
|
+
// Blocklist entries as canonical CIDRs, per address family: an exact address becomes /32 (IPv4) or /128
|
|
28
|
+
// (IPv6), an IPv4-mapped address (::ffff:1.2.3.4) is IPv4, a range is written from its network address,
|
|
29
|
+
// and anything a provider would refuse (not an IP, a /0) is left out. One bad entry used to make the
|
|
30
|
+
// provider refuse the whole push; the app still blocks what is left out here.
|
|
29
31
|
function toCidrs(ips) {
|
|
30
|
-
const v4 =
|
|
31
|
-
const v6 =
|
|
32
|
+
const v4 = new Set();
|
|
33
|
+
const v6 = new Set();
|
|
32
34
|
for (const raw of ips || []) {
|
|
33
35
|
const entry = String(raw || '').trim();
|
|
34
36
|
if (!entry) continue;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
if (!entry.includes('/')) {
|
|
38
|
+
const ip = parseIp(entry);
|
|
39
|
+
if (ip) (ip.family === 4 ? v4 : v6).add(`${ip.key}/${ip.family === 4 ? 32 : 128}`);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const bits = Number(entry.slice(entry.indexOf('/') + 1));
|
|
43
|
+
const c4 = parseCidr(entry);
|
|
44
|
+
if (c4) {
|
|
45
|
+
if (bits > 0) v4.add(`${intToIp(c4.network)}/${bits}`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const c6 = parseCidr6(entry);
|
|
49
|
+
if (!c6 || c6.bits === 0) continue;
|
|
50
|
+
if (c6.bits >= 96 && (c6.network >> 32n) === 0xffffn) {
|
|
51
|
+
// ::ffff:0:0/96 and narrower are IPv4 ranges written in IPv6 form.
|
|
52
|
+
const b4 = c6.bits - 96;
|
|
53
|
+
if (b4 > 0) v4.add(`${intToIp(Number(c6.network & 0xffffffffn) >>> 0)}/${b4}`);
|
|
54
|
+
} else {
|
|
55
|
+
v6.add(`${bigIntToIpv6(c6.network)}/${c6.bits}`);
|
|
56
|
+
}
|
|
40
57
|
}
|
|
41
|
-
return { v4, v6 };
|
|
58
|
+
return { v4: [...v4], v6: [...v6] };
|
|
42
59
|
}
|
|
43
60
|
|
|
44
61
|
function prefixLength(cidr) {
|
package/package.json
CHANGED