@liberstudio/cloudflare-list 2.4.8 → 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"],
|
|
@@ -120,9 +132,7 @@ export class AppModule {}
|
|
|
120
132
|
|
|
121
133
|
| Opzione | Tipo | Obbligatorio | Descrizione |
|
|
122
134
|
|---|---|---|---|
|
|
123
|
-
| `
|
|
124
|
-
| `accountId` | `string` | si | ID account Cloudflare |
|
|
125
|
-
| `listId` | `string` | si | ID della IP List Cloudflare |
|
|
135
|
+
| `accounts` | `CloudflareAccount[]` | si | Account Cloudflare su cui bannare (almeno uno), vedi sotto |
|
|
126
136
|
| `comment` | `string` | si | Commento aggiunto ad ogni IP bloccato |
|
|
127
137
|
| `logPath` | `string` | si | Percorso del file di log degli attacchi |
|
|
128
138
|
| `allowedIps` | `string[]` | no | IP o CIDR mai bloccati (whitelist, supporta IPv4 e IPv6) |
|
|
@@ -133,6 +143,38 @@ export class AppModule {}
|
|
|
133
143
|
| `telegramNotify` | `{ chatId: string }` | no | Invia una notifica Telegram quando un IP viene bannato |
|
|
134
144
|
| `notifyProvider` | `"whatsapp" \| "telegram"` | no | Canale di notifica da usare (default: vedi sotto) |
|
|
135
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
|
+
|
|
136
178
|
### `notifyProvider` — scelta del canale di notifica
|
|
137
179
|
|
|
138
180
|
Ad ogni ban viene inviata **una sola** notifica, sul canale scelto:
|
|
@@ -176,8 +218,11 @@ Il messaggio inviato sarà:
|
|
|
176
218
|
Provenienza: Blocked by liberstudio/cloudflare-list
|
|
177
219
|
IP: 1.2.3.4
|
|
178
220
|
Subnet bloccata: 1.2.0.0/16
|
|
221
|
+
Account: 2/2
|
|
179
222
|
```
|
|
180
223
|
|
|
224
|
+
`Account` indica su quanti account configurati il ban è andato a buon fine. La notifica è sempre **una** per ban.
|
|
225
|
+
|
|
181
226
|
Variabili d'ambiente richieste (lette automaticamente da `process.env`):
|
|
182
227
|
```env
|
|
183
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);
|
|
@@ -114,11 +114,30 @@ let AttacksService = class AttacksService {
|
|
|
114
114
|
return null;
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
+
/** Aggiunge il CIDR alle liste di tutti gli account; ritorna le risposte degli account riusciti. */
|
|
117
118
|
async updateIpList(cidr) {
|
|
118
|
-
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;
|
|
119
139
|
const url = `${this.API_URL}/${accountId}/rules/lists/${listId}/items`;
|
|
120
|
-
|
|
121
|
-
const body = [{ ip: cidr, comment }];
|
|
140
|
+
const body = [{ ip: cidr, comment: this.options.comment }];
|
|
122
141
|
try {
|
|
123
142
|
const response = await (0, rxjs_1.firstValueFrom)(this.httpService.post(url, body, {
|
|
124
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"`);
|