@liberstudio/cloudflare-list 2.3.4 → 2.3.8
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/README.md
CHANGED
|
@@ -11,14 +11,18 @@ Request → 404 rilevato dal middleware
|
|
|
11
11
|
└─ IP estratto (cf-connecting-ip / x-real-ip / x-forwarded-for)
|
|
12
12
|
└─ throttle 5s per IP
|
|
13
13
|
└─ log su file (se path non è silentPath)
|
|
14
|
-
└─
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
└─ allowedIps? → skip (log only)
|
|
15
|
+
└─ IP Cloudflare? → skip (log only)
|
|
16
|
+
└─ ASN lookup su api.hackertarget.com
|
|
17
|
+
├─ range trovato → blocca intera subnet su Cloudflare
|
|
18
|
+
└─ fallback → blocca singolo IP /32 o /128
|
|
17
19
|
```
|
|
18
20
|
|
|
19
21
|
### Filtraggio automatico
|
|
20
22
|
|
|
21
23
|
Il modulo ignora automaticamente:
|
|
24
|
+
- IP nella whitelist `allowedIps` (configurazione statica)
|
|
25
|
+
- IP appartenenti ai range ufficiali Cloudflare (scaricati e cachati 24h)
|
|
22
26
|
- IP privati (10.x, 192.168.x, 172.16–31.x)
|
|
23
27
|
- Loopback (127.0.0.1, ::1)
|
|
24
28
|
- IPv4-mapped IPv6 (::ffff:x.x.x.x → convertiti in IPv4)
|
|
@@ -68,9 +72,11 @@ import { CloudflareAttacksModule } from "@liberstudio/cloudflare-list";
|
|
|
68
72
|
apiToken: "your-cloudflare-api-token",
|
|
69
73
|
comment: "Blocked by liberstudio/cloudflare-list",
|
|
70
74
|
logPath: "/var/log/nestjs-attacks.log",
|
|
75
|
+
allowedIps: ["1.2.3.4", "10.0.0.0/8"],
|
|
71
76
|
excludedPaths: ["/api/health", "/api/webhook"],
|
|
72
77
|
silentPaths: ["/auth/me", "/auth/refresh"],
|
|
73
78
|
verbose: false,
|
|
79
|
+
whatsappNotify: { phone: "393331234567" },
|
|
74
80
|
}),
|
|
75
81
|
],
|
|
76
82
|
})
|
|
@@ -93,9 +99,13 @@ import { CloudflareAttacksModule } from "@liberstudio/cloudflare-list";
|
|
|
93
99
|
listId: config.getOrThrow<string>("CLOUDFLARE_LIST_ID"),
|
|
94
100
|
comment: config.get<string>("CLOUDFLARE_LIST_COMMENT") || "Blocked",
|
|
95
101
|
logPath: config.get<string>("CLOUDFLARE_LIST_LOG_PATH") || "/var/log/nestjs-attacks.log",
|
|
102
|
+
allowedIps: ["1.2.3.4", "10.0.0.0/8"],
|
|
96
103
|
excludedPaths: ["/api/health", "/api/webhook", /^\/api\/public\/.*/],
|
|
97
104
|
silentPaths: ["/auth/me", "/auth/refresh"],
|
|
98
105
|
verbose: false,
|
|
106
|
+
whatsappNotify: {
|
|
107
|
+
phone: config.getOrThrow<string>("WA_NOTIFY_PHONE"),
|
|
108
|
+
},
|
|
99
109
|
}),
|
|
100
110
|
}),
|
|
101
111
|
],
|
|
@@ -114,9 +124,52 @@ export class AppModule {}
|
|
|
114
124
|
| `listId` | `string` | si | ID della IP List Cloudflare |
|
|
115
125
|
| `comment` | `string` | si | Commento aggiunto ad ogni IP bloccato |
|
|
116
126
|
| `logPath` | `string` | si | Percorso del file di log degli attacchi |
|
|
127
|
+
| `allowedIps` | `string[]` | no | IP o CIDR mai bloccati (whitelist, supporta IPv4 e IPv6) |
|
|
117
128
|
| `excludedPaths` | `(string \| RegExp)[]` | no | Path esclusi da ogni rilevamento e log |
|
|
118
129
|
| `silentPaths` | `(string \| RegExp)[]` | no | Path bloccati su Cloudflare ma senza log su file |
|
|
119
130
|
| `verbose` | `boolean` | no | Se `true`, il log include body, query, params e stack |
|
|
131
|
+
| `whatsappNotify` | `{ phone: string }` | no | Invia una notifica WhatsApp quando un IP viene bannato |
|
|
132
|
+
|
|
133
|
+
### `whatsappNotify` — notifiche ban su WhatsApp
|
|
134
|
+
|
|
135
|
+
Quando configurato, invia un messaggio WhatsApp ad un numero specifico ogni volta che un IP viene bloccato su Cloudflare. Richiede le variabili d'ambiente `WA_URL`, `WA_USER`, `WA_PASSWORD` e `WA_DEVICE_ID` già presenti nel progetto (lette automaticamente da `process.env`).
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
whatsappNotify: {
|
|
139
|
+
phone: "393331234567", // numero con prefisso internazionale, senza +
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Il messaggio inviato sarà:
|
|
144
|
+
```
|
|
145
|
+
🚨 IP bannato su Cloudflare
|
|
146
|
+
IP: 1.2.3.4
|
|
147
|
+
Subnet bloccata: 1.2.0.0/16
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Variabili d'ambiente richieste (lette automaticamente da `process.env`):
|
|
151
|
+
```env
|
|
152
|
+
WA_URL=https://messaggistica.liberdesign.eu
|
|
153
|
+
WA_USER=your-user
|
|
154
|
+
WA_PASSWORD=your-password
|
|
155
|
+
WA_DEVICE_ID=your-device-id
|
|
156
|
+
WA_NOTIFY_PHONE=393331234567
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### `allowedIps` — whitelist server
|
|
160
|
+
|
|
161
|
+
Lista di IP o subnet CIDR che non vengono mai bloccati, indipendentemente dal comportamento. Utile per i propri server, proxy interni, CDN proprietarie. Supporta IPv4, CIDR IPv4, IPv6 e CIDR IPv6:
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
allowedIps: [
|
|
165
|
+
"1.2.3.4", // IP singolo IPv4
|
|
166
|
+
"10.0.0.0/8", // subnet IPv4
|
|
167
|
+
"2001:db8::1", // IP singolo IPv6
|
|
168
|
+
"2001:db8::/32", // subnet IPv6
|
|
169
|
+
]
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
> I range ufficiali Cloudflare sono esclusi automaticamente senza bisogno di aggiungerli qui.
|
|
120
173
|
|
|
121
174
|
### `excludedPaths` vs `silentPaths`
|
|
122
175
|
|
|
@@ -8,6 +8,7 @@ export declare class AttacksService {
|
|
|
8
8
|
private readonly ASN_LOOKUP_URL;
|
|
9
9
|
constructor(httpService: HttpService, options: CloudflareAttacksOptions);
|
|
10
10
|
blockIp(rawIp: string): Promise<CloudflareResponse | null>;
|
|
11
|
+
private sendWhatsappNotification;
|
|
11
12
|
private lookupAsnRange;
|
|
12
13
|
updateIpList(cidr: string): Promise<CloudflareResponse>;
|
|
13
14
|
}
|
package/dist/attacks.service.js
CHANGED
|
@@ -31,19 +31,46 @@ let AttacksService = class AttacksService {
|
|
|
31
31
|
this.ASN_LOOKUP_URL = `https://api.hackertarget.com/aslookup`;
|
|
32
32
|
}
|
|
33
33
|
async blockIp(rawIp) {
|
|
34
|
+
if ((0, utils_1.isAllowedIp)(rawIp, this.options.allowedIps ?? [])) {
|
|
35
|
+
this.logger.log(`IP ${rawIp} in whitelist — ban saltato`);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
34
38
|
if (await (0, utils_1.isCloudflareIp)(rawIp)) {
|
|
35
39
|
this.logger.log(`IP ${rawIp} appartiene al range Cloudflare — ban saltato`);
|
|
36
40
|
return null;
|
|
37
41
|
}
|
|
38
42
|
const asnRange = await this.lookupAsnRange(rawIp);
|
|
39
43
|
if (asnRange) {
|
|
40
|
-
this.logger.
|
|
41
|
-
|
|
44
|
+
this.logger.error(`ASN range trovato per ${rawIp}: ${asnRange} — blocco intera subnet`);
|
|
45
|
+
const result = await this.updateIpList(asnRange);
|
|
46
|
+
this.sendWhatsappNotification(rawIp, asnRange);
|
|
47
|
+
return result;
|
|
42
48
|
}
|
|
43
49
|
const cidr = (0, utils_1.normalizeIp)(rawIp);
|
|
44
50
|
if (!cidr)
|
|
45
51
|
throw new Error(`IP non valido: ${rawIp}`);
|
|
46
|
-
|
|
52
|
+
const result = await this.updateIpList(cidr);
|
|
53
|
+
this.sendWhatsappNotification(rawIp, cidr);
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
sendWhatsappNotification(rawIp, cidr) {
|
|
57
|
+
const wa = this.options.whatsappNotify;
|
|
58
|
+
if (!wa)
|
|
59
|
+
return;
|
|
60
|
+
const url = process.env["WA_URL"];
|
|
61
|
+
const user = process.env["WA_USER"];
|
|
62
|
+
const password = process.env["WA_PASSWORD"];
|
|
63
|
+
const deviceId = process.env["WA_DEVICE_ID"];
|
|
64
|
+
if (!url || !user || !password || !deviceId) {
|
|
65
|
+
this.logger.warn("Notifica WhatsApp saltata: WA_URL, WA_USER, WA_PASSWORD o WA_DEVICE_ID mancanti");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const auth = Buffer.from(`${user}:${password}`).toString("base64");
|
|
69
|
+
const message = `🚨 IP bannato su Cloudflare\nIP: ${rawIp}\nSubnet bloccata: ${cidr}`;
|
|
70
|
+
(0, rxjs_1.firstValueFrom)(this.httpService.post(`${url}/send/message`, { phone: wa.phone, message }, { headers: { Authorization: `Basic ${auth}`, "X-Device-Id": deviceId } })).catch((err) => {
|
|
71
|
+
const msg = err instanceof Error ? err.message : "Errore sconosciuto";
|
|
72
|
+
this.logger.warn(`Notifica WhatsApp fallita: ${msg}`);
|
|
73
|
+
});
|
|
47
74
|
}
|
|
48
75
|
async lookupAsnRange(ip) {
|
|
49
76
|
try {
|
|
@@ -57,7 +84,7 @@ let AttacksService = class AttacksService {
|
|
|
57
84
|
async updateIpList(cidr) {
|
|
58
85
|
const { accountId, listId, apiToken, comment } = this.options;
|
|
59
86
|
const url = `${this.API_URL}/${accountId}/rules/lists/${listId}/items`;
|
|
60
|
-
this.logger.
|
|
87
|
+
this.logger.error(`Aggiungo ${cidr} alla lista Cloudflare`);
|
|
61
88
|
const body = [{ ip: cidr, comment }];
|
|
62
89
|
try {
|
|
63
90
|
const response = await (0, rxjs_1.firstValueFrom)(this.httpService.post(url, body, {
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
+
export interface WhatsappNotifyOptions {
|
|
2
|
+
phone: string;
|
|
3
|
+
}
|
|
1
4
|
export interface CloudflareAttacksOptions {
|
|
2
5
|
apiToken: string;
|
|
3
6
|
accountId: string;
|
|
4
7
|
listId: string;
|
|
5
8
|
comment: string;
|
|
6
9
|
logPath: string;
|
|
10
|
+
allowedIps?: string[];
|
|
7
11
|
excludedPaths?: (string | RegExp)[];
|
|
8
12
|
silentPaths?: (string | RegExp)[];
|
|
9
13
|
verbose?: boolean;
|
|
14
|
+
whatsappNotify?: WhatsappNotifyOptions;
|
|
10
15
|
}
|
|
11
16
|
export interface CloudflareAttacksAsyncOptions {
|
|
12
17
|
imports?: any[];
|
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isAllowedIp = isAllowedIp;
|
|
6
7
|
exports.isCloudflareIp = isCloudflareIp;
|
|
7
8
|
const axios_1 = __importDefault(require("axios"));
|
|
8
9
|
const net_1 = require("net");
|
|
@@ -60,6 +61,29 @@ function isInCidrV6(ip, cidr) {
|
|
|
60
61
|
const mask = ((1n << 128n) - 1n) ^ ((1n << BigInt(128 - prefix)) - 1n);
|
|
61
62
|
return (ipv6ToBigInt(ip) & mask) === (ipv6ToBigInt(range) & mask);
|
|
62
63
|
}
|
|
64
|
+
function isAllowedIp(ip, allowedIps) {
|
|
65
|
+
if (!ip || !allowedIps.length)
|
|
66
|
+
return false;
|
|
67
|
+
let pure = ip.includes("/") ? ip.split("/")[0] : ip;
|
|
68
|
+
pure = pure.trim();
|
|
69
|
+
if (pure.startsWith("::ffff:"))
|
|
70
|
+
pure = pure.slice("::ffff:".length);
|
|
71
|
+
const version = (0, net_1.isIP)(pure);
|
|
72
|
+
if (!version)
|
|
73
|
+
return false;
|
|
74
|
+
return allowedIps.some((entry) => {
|
|
75
|
+
let entryPure = entry.includes("/") ? entry.split("/")[0] : entry;
|
|
76
|
+
entryPure = entryPure.trim();
|
|
77
|
+
if (entryPure.startsWith("::ffff:"))
|
|
78
|
+
entryPure = entryPure.slice("::ffff:".length);
|
|
79
|
+
const entryVersion = (0, net_1.isIP)(entryPure);
|
|
80
|
+
if (!entryVersion || entryVersion !== version)
|
|
81
|
+
return false;
|
|
82
|
+
return version === 4
|
|
83
|
+
? isInCidrV4(pure, entry)
|
|
84
|
+
: isInCidrV6(pure, entry);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
63
87
|
async function isCloudflareIp(ip) {
|
|
64
88
|
if (!ip)
|
|
65
89
|
return false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liberstudio/cloudflare-list",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.8",
|
|
4
4
|
"description": "Modulo NestJS per gestione IP List Cloudflare",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"build": "npx tsc",
|
|
17
17
|
"prepublishOnly": "npm run build",
|
|
18
18
|
"version:bump": "npx tsx src/scripts/version-bump.ts",
|
|
19
|
-
"pub": "npm publish --access public"
|
|
19
|
+
"pub": "npm run build && npm publish --access public"
|
|
20
20
|
},
|
|
21
21
|
"publishConfig": {
|
|
22
22
|
"access": "public"
|