@liberstudio/cloudflare-list 2.4.7 → 3.0.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/README.md
CHANGED
|
@@ -47,6 +47,8 @@ Il modulo ignora automaticamente:
|
|
|
47
47
|
- Action: `Block`
|
|
48
48
|
6. Salvare la regola
|
|
49
49
|
|
|
50
|
+
Con più account Cloudflare ripetere i passi per ogni account: ognuno ha la sua lista, il suo token e la sua regola WAF.
|
|
51
|
+
|
|
50
52
|
---
|
|
51
53
|
|
|
52
54
|
## Installazione
|
|
@@ -67,9 +69,10 @@ import { CloudflareAttacksModule } from "@liberstudio/cloudflare-list";
|
|
|
67
69
|
@Module({
|
|
68
70
|
imports: [
|
|
69
71
|
CloudflareAttacksModule.forRoot({
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
accounts: [
|
|
73
|
+
{ accountId: "abc123", listId: "def456", apiToken: "token-account-1" },
|
|
74
|
+
{ accountId: "ghi789", listId: "jkl012", apiToken: "token-account-2" },
|
|
75
|
+
],
|
|
73
76
|
comment: "Blocked by liberstudio/cloudflare-list",
|
|
74
77
|
logPath: "/var/log/nestjs-attacks.log",
|
|
75
78
|
allowedIps: ["1.2.3.4", "10.0.0.0/8"],
|
|
@@ -94,9 +97,18 @@ import { CloudflareAttacksModule } from "@liberstudio/cloudflare-list";
|
|
|
94
97
|
imports: [ConfigModule],
|
|
95
98
|
inject: [ConfigService],
|
|
96
99
|
useFactory: (config: ConfigService) => ({
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
accounts: [
|
|
101
|
+
{
|
|
102
|
+
apiToken: config.getOrThrow<string>("CLOUDFLARE_API_TOKEN"),
|
|
103
|
+
accountId: config.getOrThrow<string>("CLOUDFLARE_ACCOUNT_ID"),
|
|
104
|
+
listId: config.getOrThrow<string>("CLOUDFLARE_LIST_ID"),
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
apiToken: config.getOrThrow<string>("CLOUDFLARE_2_API_TOKEN"),
|
|
108
|
+
accountId: config.getOrThrow<string>("CLOUDFLARE_2_ACCOUNT_ID"),
|
|
109
|
+
listId: config.getOrThrow<string>("CLOUDFLARE_2_LIST_ID"),
|
|
110
|
+
},
|
|
111
|
+
],
|
|
100
112
|
comment: config.get<string>("CLOUDFLARE_LIST_COMMENT") || "Blocked",
|
|
101
113
|
logPath: config.get<string>("CLOUDFLARE_LIST_LOG_PATH") || "/var/log/nestjs-attacks.log",
|
|
102
114
|
allowedIps: ["1.2.3.4", "10.0.0.0/8"],
|
|
@@ -106,6 +118,7 @@ import { CloudflareAttacksModule } from "@liberstudio/cloudflare-list";
|
|
|
106
118
|
telegramNotify: {
|
|
107
119
|
chatId: config.getOrThrow<string>("TELEGRAM_NOTIFY_CHAT_ID"),
|
|
108
120
|
},
|
|
121
|
+
notifyProvider: config.get<"whatsapp" | "telegram">("NOTIFY_PROVIDER"), // assente → scelta automatica
|
|
109
122
|
}),
|
|
110
123
|
}),
|
|
111
124
|
],
|
|
@@ -119,9 +132,7 @@ export class AppModule {}
|
|
|
119
132
|
|
|
120
133
|
| Opzione | Tipo | Obbligatorio | Descrizione |
|
|
121
134
|
|---|---|---|---|
|
|
122
|
-
| `
|
|
123
|
-
| `accountId` | `string` | si | ID account Cloudflare |
|
|
124
|
-
| `listId` | `string` | si | ID della IP List Cloudflare |
|
|
135
|
+
| `accounts` | `CloudflareAccount[]` | si | Account Cloudflare su cui bannare (almeno uno), vedi sotto |
|
|
125
136
|
| `comment` | `string` | si | Commento aggiunto ad ogni IP bloccato |
|
|
126
137
|
| `logPath` | `string` | si | Percorso del file di log degli attacchi |
|
|
127
138
|
| `allowedIps` | `string[]` | no | IP o CIDR mai bloccati (whitelist, supporta IPv4 e IPv6) |
|
|
@@ -132,6 +143,38 @@ export class AppModule {}
|
|
|
132
143
|
| `telegramNotify` | `{ chatId: string }` | no | Invia una notifica Telegram quando un IP viene bannato |
|
|
133
144
|
| `notifyProvider` | `"whatsapp" \| "telegram"` | no | Canale di notifica da usare (default: vedi sotto) |
|
|
134
145
|
|
|
146
|
+
### `accounts` — ban su più account Cloudflare
|
|
147
|
+
|
|
148
|
+
Ogni elemento di `accounts` è un `CloudflareAccount`:
|
|
149
|
+
|
|
150
|
+
| Campo | Tipo | Obbligatorio | Descrizione |
|
|
151
|
+
|---|---|---|---|
|
|
152
|
+
| `apiToken` | `string` | si | Token API Cloudflare con accesso all'account |
|
|
153
|
+
| `accountId` | `string` | si | ID account Cloudflare |
|
|
154
|
+
| `listId` | `string` | si | ID della IP List dell'account |
|
|
155
|
+
|
|
156
|
+
Ad ogni ban l'IP (o la subnet) viene aggiunto **in parallelo** alle liste di tutti gli account:
|
|
157
|
+
|
|
158
|
+
- se un account fallisce, l'errore viene loggato con il suo `accountId` e gli altri proseguono;
|
|
159
|
+
- se falliscono **tutti**, `updateIpList()` lancia `HttpException` con il primo errore;
|
|
160
|
+
- `updateIpList()` e `blockIp()` ritornano un `CloudflareResponse[]` con le risposte degli account riusciti.
|
|
161
|
+
|
|
162
|
+
Se lo stesso token ha accesso a più account, basta ripeterlo in ogni elemento.
|
|
163
|
+
|
|
164
|
+
### Migrazione dalla 2.x
|
|
165
|
+
|
|
166
|
+
La 3.0.0 è un breaking change: `apiToken`, `accountId` e `listId` non stanno più alla radice delle opzioni, ma dentro `accounts`.
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// 2.x
|
|
170
|
+
{ apiToken: "...", accountId: "...", listId: "...", comment: "...", logPath: "..." }
|
|
171
|
+
|
|
172
|
+
// 3.x
|
|
173
|
+
{ accounts: [{ apiToken: "...", accountId: "...", listId: "..." }], comment: "...", logPath: "..." }
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Inoltre `updateIpList()` ritorna `CloudflareResponse[]` invece di `CloudflareResponse`, e `blockIp()` ritorna `CloudflareResponse[] | null`.
|
|
177
|
+
|
|
135
178
|
### `notifyProvider` — scelta del canale di notifica
|
|
136
179
|
|
|
137
180
|
Ad ogni ban viene inviata **una sola** notifica, sul canale scelto:
|
|
@@ -156,6 +199,7 @@ Per ricavare il `chatId`: scrivi un messaggio al bot (o aggiungilo al gruppo), p
|
|
|
156
199
|
```env
|
|
157
200
|
TELEGRAM_BOT_TOKEN=123456:ABC-your-bot-token
|
|
158
201
|
TELEGRAM_NOTIFY_CHAT_ID=123456789
|
|
202
|
+
NOTIFY_PROVIDER=telegram # opzionale: whatsapp | telegram
|
|
159
203
|
```
|
|
160
204
|
|
|
161
205
|
### `whatsappNotify` — notifiche ban su WhatsApp
|
|
@@ -174,8 +218,11 @@ Il messaggio inviato sarà:
|
|
|
174
218
|
Provenienza: Blocked by liberstudio/cloudflare-list
|
|
175
219
|
IP: 1.2.3.4
|
|
176
220
|
Subnet bloccata: 1.2.0.0/16
|
|
221
|
+
Account: 2/2
|
|
177
222
|
```
|
|
178
223
|
|
|
224
|
+
`Account` indica su quanti account configurati il ban è andato a buon fine. La notifica è sempre **una** per ban.
|
|
225
|
+
|
|
179
226
|
Variabili d'ambiente richieste (lette automaticamente da `process.env`):
|
|
180
227
|
```env
|
|
181
228
|
WA_URL=https://messaggistica.liberdesign.eu
|
|
@@ -7,11 +7,13 @@ export declare class AttacksService {
|
|
|
7
7
|
private readonly API_URL;
|
|
8
8
|
private readonly ASN_LOOKUP_URL;
|
|
9
9
|
constructor(httpService: HttpService, options: CloudflareAttacksOptions);
|
|
10
|
-
blockIp(rawIp: string): Promise<CloudflareResponse | null>;
|
|
10
|
+
blockIp(rawIp: string): Promise<CloudflareResponse[] | null>;
|
|
11
11
|
private sendNotification;
|
|
12
12
|
private isWhatsappConfigured;
|
|
13
13
|
private sendTelegramNotification;
|
|
14
14
|
private sendWhatsappNotification;
|
|
15
15
|
private lookupAsnRange;
|
|
16
|
-
|
|
16
|
+
/** Aggiunge il CIDR alle liste di tutti gli account; ritorna le risposte degli account riusciti. */
|
|
17
|
+
updateIpList(cidr: string): Promise<CloudflareResponse[]>;
|
|
18
|
+
private addToAccountList;
|
|
17
19
|
}
|
package/dist/attacks.service.js
CHANGED
|
@@ -47,7 +47,7 @@ let AttacksService = class AttacksService {
|
|
|
47
47
|
if (asnRange && !(await (0, utils_1.googleBotOverlapsCidr)(asnRange))) {
|
|
48
48
|
this.logger.error(`ASN range trovato per ${rawIp}: ${asnRange} — blocco intera subnet`);
|
|
49
49
|
const result = await this.updateIpList(asnRange);
|
|
50
|
-
this.sendNotification(rawIp, asnRange);
|
|
50
|
+
this.sendNotification(rawIp, asnRange, result.length);
|
|
51
51
|
return result;
|
|
52
52
|
}
|
|
53
53
|
if (asnRange) {
|
|
@@ -57,11 +57,11 @@ let AttacksService = class AttacksService {
|
|
|
57
57
|
if (!cidr)
|
|
58
58
|
throw new Error(`IP non valido: ${rawIp}`);
|
|
59
59
|
const result = await this.updateIpList(cidr);
|
|
60
|
-
this.sendNotification(rawIp, cidr);
|
|
60
|
+
this.sendNotification(rawIp, cidr, result.length);
|
|
61
61
|
return result;
|
|
62
62
|
}
|
|
63
|
-
sendNotification(rawIp, cidr) {
|
|
64
|
-
const message = `🚨 IP bannato su Cloudflare\nProvenienza: ${this.options.comment}\nIP: ${rawIp}\nSubnet bloccata: ${cidr}`;
|
|
63
|
+
sendNotification(rawIp, cidr, okAccounts) {
|
|
64
|
+
const message = `🚨 IP bannato su Cloudflare\nProvenienza: ${this.options.comment}\nIP: ${rawIp}\nSubnet bloccata: ${cidr}\nAccount: ${okAccounts}/${this.options.accounts.length}`;
|
|
65
65
|
const provider = this.options.notifyProvider ?? (this.isWhatsappConfigured() ? "whatsapp" : "telegram");
|
|
66
66
|
if (provider === "whatsapp")
|
|
67
67
|
this.sendWhatsappNotification(message);
|
|
@@ -83,7 +83,8 @@ let AttacksService = class AttacksService {
|
|
|
83
83
|
}
|
|
84
84
|
(0, rxjs_1.firstValueFrom)(this.httpService.post(`https://api.telegram.org/bot${token}/sendMessage`, { chat_id: tg.chatId, text: message })).catch((err) => {
|
|
85
85
|
const msg = err instanceof Error ? err.message : "Errore sconosciuto";
|
|
86
|
-
|
|
86
|
+
const description = axios_2.default.isAxiosError(err) ? err.response?.data?.description : undefined;
|
|
87
|
+
this.logger.warn(`Notifica Telegram fallita: ${msg}${description ? ` (${description})` : ""}`);
|
|
87
88
|
});
|
|
88
89
|
}
|
|
89
90
|
sendWhatsappNotification(message) {
|
|
@@ -113,11 +114,30 @@ let AttacksService = class AttacksService {
|
|
|
113
114
|
return null;
|
|
114
115
|
}
|
|
115
116
|
}
|
|
117
|
+
/** Aggiunge il CIDR alle liste di tutti gli account; ritorna le risposte degli account riusciti. */
|
|
116
118
|
async updateIpList(cidr) {
|
|
117
|
-
const {
|
|
119
|
+
const { accounts } = this.options;
|
|
120
|
+
this.logger.error(`Aggiungo ${cidr} alle liste Cloudflare (${accounts.length} account)`);
|
|
121
|
+
const results = await Promise.allSettled(accounts.map((account) => this.addToAccountList(account, cidr)));
|
|
122
|
+
const ok = [];
|
|
123
|
+
let firstError;
|
|
124
|
+
results.forEach((r, i) => {
|
|
125
|
+
if (r.status === "fulfilled") {
|
|
126
|
+
ok.push(r.value);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
firstError ??= r.reason;
|
|
130
|
+
const msg = r.reason instanceof Error ? r.reason.message : "Errore sconosciuto";
|
|
131
|
+
this.logger.error(`Aggiunta di ${cidr} fallita sull'account ${accounts[i].accountId}: ${msg}`);
|
|
132
|
+
});
|
|
133
|
+
if (ok.length === 0)
|
|
134
|
+
throw firstError;
|
|
135
|
+
return ok;
|
|
136
|
+
}
|
|
137
|
+
async addToAccountList(account, cidr) {
|
|
138
|
+
const { accountId, listId, apiToken } = account;
|
|
118
139
|
const url = `${this.API_URL}/${accountId}/rules/lists/${listId}/items`;
|
|
119
|
-
|
|
120
|
-
const body = [{ ip: cidr, comment }];
|
|
140
|
+
const body = [{ ip: cidr, comment: this.options.comment }];
|
|
121
141
|
try {
|
|
122
142
|
const response = await (0, rxjs_1.firstValueFrom)(this.httpService.post(url, body, {
|
|
123
143
|
headers: {
|
|
@@ -5,10 +5,13 @@ export interface TelegramNotifyOptions {
|
|
|
5
5
|
chatId: string;
|
|
6
6
|
}
|
|
7
7
|
export type NotifyProvider = "whatsapp" | "telegram";
|
|
8
|
-
export interface
|
|
8
|
+
export interface CloudflareAccount {
|
|
9
9
|
apiToken: string;
|
|
10
10
|
accountId: string;
|
|
11
11
|
listId: string;
|
|
12
|
+
}
|
|
13
|
+
export interface CloudflareAttacksOptions {
|
|
14
|
+
accounts: CloudflareAccount[];
|
|
12
15
|
comment: string;
|
|
13
16
|
logPath: string;
|
|
14
17
|
allowedIps?: string[];
|
|
@@ -20,4 +20,10 @@ if (minor >= 10) {
|
|
|
20
20
|
pkg.version = `${major}.${minor}.${patch}`;
|
|
21
21
|
// Scrittura package.json
|
|
22
22
|
(0, node_fs_1.writeFileSync)(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
23
|
+
// Allineamento package-lock.json (versione radice e del pacchetto "")
|
|
24
|
+
const lockPath = (0, node_path_1.resolve)(__dirname, '../../package-lock.json');
|
|
25
|
+
const lock = JSON.parse((0, node_fs_1.readFileSync)(lockPath, 'utf8'));
|
|
26
|
+
lock.version = pkg.version;
|
|
27
|
+
lock.packages[''].version = pkg.version;
|
|
28
|
+
(0, node_fs_1.writeFileSync)(lockPath, JSON.stringify(lock, null, 2) + '\n');
|
|
23
29
|
console.log(`Version updated to ${pkg.version}`);
|
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.validateOptions = validateOptions;
|
|
4
|
+
function isNonEmptyString(value) {
|
|
5
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
6
|
+
}
|
|
4
7
|
function validateOptions(opzioni) {
|
|
5
8
|
if (!opzioni)
|
|
6
9
|
throw new Error("CloudflareAttacksOptions è obbligatorio");
|
|
7
|
-
const campiObbligatori = ["
|
|
10
|
+
const campiObbligatori = ["comment", "logPath"];
|
|
8
11
|
for (const campo of campiObbligatori) {
|
|
9
|
-
|
|
10
|
-
if (typeof valore !== "string" || valore.trim().length === 0) {
|
|
12
|
+
if (!isNonEmptyString(opzioni[campo])) {
|
|
11
13
|
throw new Error(`CloudflareAttacksOptions.${campo} deve essere una stringa non vuota`);
|
|
12
14
|
}
|
|
13
15
|
}
|
|
16
|
+
if (!Array.isArray(opzioni.accounts) || opzioni.accounts.length === 0) {
|
|
17
|
+
throw new Error("CloudflareAttacksOptions.accounts deve essere un array non vuoto");
|
|
18
|
+
}
|
|
19
|
+
const campiAccount = ["apiToken", "accountId", "listId"];
|
|
20
|
+
opzioni.accounts.forEach((account, i) => {
|
|
21
|
+
for (const campo of campiAccount) {
|
|
22
|
+
if (!isNonEmptyString(account?.[campo])) {
|
|
23
|
+
throw new Error(`CloudflareAttacksOptions.accounts[${i}].${campo} deve essere una stringa non vuota`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
14
27
|
const provider = opzioni.notifyProvider;
|
|
15
28
|
if (provider !== undefined && provider !== "whatsapp" && provider !== "telegram") {
|
|
16
29
|
throw new Error(`CloudflareAttacksOptions.notifyProvider deve essere "whatsapp" o "telegram"`);
|