@liberstudio/cloudflare-list 2.0.14 → 2.0.16
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 +6 -4
- package/dist/attacks.module.js +9 -3
- package/dist/attacks.service.js +2 -1
- package/dist/filters/all-exceptions.filter.js +7 -7
- package/dist/interfaces/cloudflare-attacks.interface.d.ts +1 -0
- package/dist/middleware/attack-logger.middleware.d.ts +12 -9
- package/dist/middleware/attack-logger.middleware.js +72 -78
- package/dist/utils/cloudflare.constants.d.ts +1 -0
- package/dist/utils/cloudflare.constants.js +4 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/options.validator.d.ts +2 -0
- package/dist/utils/options.validator.js +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
3. Manage Account > Configuration > List > Create List
|
|
8
8
|
4. Account API tokens > Create Token > Create Custom Token (Permission: Account > Account WAF > Edit, Account > Account Filter List > Edit)
|
|
9
9
|
5. Salvare il token API senza scadenza
|
|
10
|
-
6. Domains > Scegliere il dominio da utilizzare
|
|
10
|
+
6. Domains > Scegliere il dominio da utilizzare
|
|
11
11
|
7. Andare nella categoria Security > Security Rules > Create rule > Custom Rules
|
|
12
|
-
8. Nome: <Name>, Field: <IP Source Address , is in, scegli la lista creata prima>, Action: Block
|
|
12
|
+
8. Nome: <Name>, Field: <IP Source Address , is in, scegli la lista creata prima>, Action: Block
|
|
13
13
|
9. Salvare la regola
|
|
14
14
|
|
|
15
15
|
## Installazione
|
|
@@ -34,6 +34,7 @@ import { CloudflareAttacksModule } from "@liberstudio/cloudflare-list";
|
|
|
34
34
|
listId: "<list_id>",
|
|
35
35
|
apiToken: "<api_token>",
|
|
36
36
|
comment: "<comment>",
|
|
37
|
+
logPath: "<logPath>",
|
|
37
38
|
}),
|
|
38
39
|
],
|
|
39
40
|
})
|
|
@@ -54,7 +55,8 @@ import { CloudflareAttacksModule } from "@liberstudio/cloudflare-list";
|
|
|
54
55
|
apiToken: config.getOrThrow<string>('CLOUDFLARE_API_TOKEN'),
|
|
55
56
|
accountId: config.getOrThrow<string>('CLOUDFLARE_ACCOUNT_ID'),
|
|
56
57
|
listId: config.getOrThrow<string>('CLOUDFLARE_LIST_ID'),
|
|
57
|
-
comment: config.get<string>('CLOUDFLARE_LIST_COMMENT') || 'Blocked'
|
|
58
|
+
comment: config.get<string>('CLOUDFLARE_LIST_COMMENT') || 'Blocked',
|
|
59
|
+
logPath: config.get<string>('CLOUDFLARE_LIST_LOG_PATH') || '/var/log/nestjs-attacks.log',
|
|
58
60
|
})
|
|
59
61
|
}),
|
|
60
62
|
],
|
|
@@ -63,4 +65,4 @@ export class AppModule {}
|
|
|
63
65
|
```
|
|
64
66
|
|
|
65
67
|
## License
|
|
66
|
-
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
68
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
package/dist/attacks.module.js
CHANGED
|
@@ -14,17 +14,19 @@ const core_1 = require("@nestjs/core");
|
|
|
14
14
|
const filters_1 = require("./filters");
|
|
15
15
|
const attacks_service_1 = require("./attacks.service");
|
|
16
16
|
const middleware_1 = require("./middleware");
|
|
17
|
+
const utils_1 = require("./utils");
|
|
17
18
|
let CloudflareAttacksModule = CloudflareAttacksModule_1 = class CloudflareAttacksModule {
|
|
18
19
|
configure(consumer) {
|
|
19
20
|
consumer.apply(middleware_1.AttackLoggerMiddleware).forRoutes("*");
|
|
20
21
|
}
|
|
21
22
|
static forRoot(options) {
|
|
23
|
+
(0, utils_1.validateOptions)(options);
|
|
22
24
|
return {
|
|
23
25
|
module: CloudflareAttacksModule_1,
|
|
24
26
|
imports: [axios_1.HttpModule],
|
|
25
27
|
providers: [
|
|
26
28
|
{
|
|
27
|
-
provide:
|
|
29
|
+
provide: utils_1.CLOUDFLARE_OPTIONS,
|
|
28
30
|
useValue: options,
|
|
29
31
|
},
|
|
30
32
|
],
|
|
@@ -36,8 +38,12 @@ let CloudflareAttacksModule = CloudflareAttacksModule_1 = class CloudflareAttack
|
|
|
36
38
|
imports: [axios_1.HttpModule, ...(options.imports || [])],
|
|
37
39
|
providers: [
|
|
38
40
|
{
|
|
39
|
-
provide:
|
|
40
|
-
useFactory:
|
|
41
|
+
provide: utils_1.CLOUDFLARE_OPTIONS,
|
|
42
|
+
useFactory: async (...args) => {
|
|
43
|
+
const opts = await options.useFactory(...args);
|
|
44
|
+
(0, utils_1.validateOptions)(opts);
|
|
45
|
+
return opts;
|
|
46
|
+
},
|
|
41
47
|
inject: options.inject || [],
|
|
42
48
|
},
|
|
43
49
|
],
|
package/dist/attacks.service.js
CHANGED
|
@@ -21,6 +21,7 @@ const common_1 = require("@nestjs/common");
|
|
|
21
21
|
const axios_1 = require("@nestjs/axios");
|
|
22
22
|
const axios_2 = __importDefault(require("axios"));
|
|
23
23
|
const rxjs_1 = require("rxjs");
|
|
24
|
+
const utils_1 = require("./utils");
|
|
24
25
|
let AttacksService = class AttacksService {
|
|
25
26
|
constructor(httpService, options) {
|
|
26
27
|
this.httpService = httpService;
|
|
@@ -57,6 +58,6 @@ let AttacksService = class AttacksService {
|
|
|
57
58
|
exports.AttacksService = AttacksService;
|
|
58
59
|
exports.AttacksService = AttacksService = __decorate([
|
|
59
60
|
(0, common_1.Injectable)(),
|
|
60
|
-
__param(1, (0, common_1.Inject)(
|
|
61
|
+
__param(1, (0, common_1.Inject)(utils_1.CLOUDFLARE_OPTIONS)),
|
|
61
62
|
__metadata("design:paramtypes", [axios_1.HttpService, Object])
|
|
62
63
|
], AttacksService);
|
|
@@ -33,13 +33,13 @@ let AllExceptionsFilter = AllExceptionsFilter_1 = class AllExceptionsFilter {
|
|
|
33
33
|
message,
|
|
34
34
|
stack: exception instanceof Error ? exception.stack : undefined,
|
|
35
35
|
};
|
|
36
|
-
const isMissingToken = message === "Invalid or missing token";
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
else {
|
|
41
|
-
|
|
42
|
-
}
|
|
36
|
+
/* const isMissingToken = message === "Invalid or missing token"; */
|
|
37
|
+
this.logger.error(`[${ip}] [${request.method}] ${request.url} → ${status}`);
|
|
38
|
+
/* if (status >= 500) {
|
|
39
|
+
this.logger.error(`[${ip}] [${request.method}] ${request.url} → ${status}`, isMissingToken ? null : JSON.stringify(errorLog, null, 2));
|
|
40
|
+
} else {
|
|
41
|
+
this.logger.warn(`[${ip}] [${request.method}] ${request.url} → ${status}`, isMissingToken ? null : JSON.stringify(errorLog, null, 2));
|
|
42
|
+
} */
|
|
43
43
|
response.status(status).json({
|
|
44
44
|
statusCode: status,
|
|
45
45
|
timestamp: errorLog.timestamp,
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import { NestMiddleware } from "@nestjs/common";
|
|
2
|
-
import { NextFunction, Request, Response } from "express";
|
|
1
|
+
import { NestMiddleware, OnModuleDestroy } from "@nestjs/common";
|
|
3
2
|
import { AttacksService } from "../attacks.service";
|
|
4
|
-
|
|
3
|
+
import type { NextFunction, Request, Response } from "express";
|
|
4
|
+
import type { CloudflareAttacksOptions } from "../interfaces";
|
|
5
|
+
export declare class AttackLoggerMiddleware implements NestMiddleware, OnModuleDestroy {
|
|
5
6
|
private readonly attSrv;
|
|
7
|
+
private readonly options;
|
|
6
8
|
private readonly logger;
|
|
7
|
-
private readonly logPath;
|
|
8
9
|
private readonly recentIps;
|
|
9
|
-
private readonly
|
|
10
|
-
private
|
|
11
|
-
constructor(attSrv: AttacksService);
|
|
10
|
+
private readonly throttleMs;
|
|
11
|
+
private readonly stream;
|
|
12
|
+
constructor(attSrv: AttacksService, options: CloudflareAttacksOptions);
|
|
12
13
|
use(req: Request, res: Response, next: NextFunction): void;
|
|
13
|
-
private
|
|
14
|
-
private processSuspiciousRequest;
|
|
14
|
+
private handleSuspicious;
|
|
15
15
|
private isThrottled;
|
|
16
16
|
private getClientIp;
|
|
17
|
+
private sanitize;
|
|
18
|
+
private ensureDirectoryExists;
|
|
19
|
+
onModuleDestroy(): void;
|
|
17
20
|
}
|
|
@@ -1,107 +1,83 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
2
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
19
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
20
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
21
5
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
22
6
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
23
7
|
};
|
|
24
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
-
var ownKeys = function(o) {
|
|
26
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
-
var ar = [];
|
|
28
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
-
return ar;
|
|
30
|
-
};
|
|
31
|
-
return ownKeys(o);
|
|
32
|
-
};
|
|
33
|
-
return function (mod) {
|
|
34
|
-
if (mod && mod.__esModule) return mod;
|
|
35
|
-
var result = {};
|
|
36
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
-
__setModuleDefault(result, mod);
|
|
38
|
-
return result;
|
|
39
|
-
};
|
|
40
|
-
})();
|
|
41
8
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
42
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
43
10
|
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
var AttackLoggerMiddleware_1;
|
|
44
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
16
|
exports.AttackLoggerMiddleware = void 0;
|
|
46
|
-
const fs = __importStar(require("fs/promises"));
|
|
47
17
|
const common_1 = require("@nestjs/common");
|
|
18
|
+
const fs_1 = require("fs");
|
|
19
|
+
const path_1 = require("path");
|
|
48
20
|
const attacks_service_1 = require("../attacks.service");
|
|
49
|
-
|
|
50
|
-
|
|
21
|
+
const utils_1 = require("../utils");
|
|
22
|
+
let AttackLoggerMiddleware = AttackLoggerMiddleware_1 = class AttackLoggerMiddleware {
|
|
23
|
+
constructor(attSrv, options) {
|
|
51
24
|
this.attSrv = attSrv;
|
|
52
|
-
this.
|
|
53
|
-
this.
|
|
54
|
-
// Throttle per IP: evita flood verso Cloudflare
|
|
25
|
+
this.options = options;
|
|
26
|
+
this.logger = new common_1.Logger(AttackLoggerMiddleware_1.name);
|
|
55
27
|
this.recentIps = new Map();
|
|
56
|
-
this.
|
|
57
|
-
|
|
58
|
-
|
|
28
|
+
this.throttleMs = 5_000;
|
|
29
|
+
if (!options.logPath || typeof options.logPath !== "string") {
|
|
30
|
+
throw new Error("CloudflareAttacksOptions.logPath deve essere una stringa valida");
|
|
31
|
+
}
|
|
32
|
+
const absolutePath = (0, path_1.resolve)(options.logPath);
|
|
33
|
+
this.ensureDirectoryExists(absolutePath);
|
|
34
|
+
this.stream = (0, fs_1.createWriteStream)(absolutePath, {
|
|
35
|
+
flags: "a",
|
|
36
|
+
encoding: "utf8",
|
|
37
|
+
highWaterMark: 64 * 1024,
|
|
38
|
+
});
|
|
39
|
+
this.stream.on("error", (err) => {
|
|
40
|
+
this.logger.error(`Errore nello stream del log (${absolutePath}): ${err.message}`);
|
|
41
|
+
});
|
|
59
42
|
}
|
|
60
43
|
use(req, res, next) {
|
|
61
44
|
res.on("finish", () => {
|
|
62
45
|
if (res.statusCode === 404) {
|
|
63
|
-
this.
|
|
46
|
+
this.handleSuspicious(req);
|
|
64
47
|
}
|
|
65
48
|
});
|
|
66
49
|
next();
|
|
67
50
|
}
|
|
68
|
-
|
|
69
|
-
this.writeQueue = this.writeQueue
|
|
70
|
-
.then(() => this.processSuspiciousRequest(req))
|
|
71
|
-
.catch((err) => {
|
|
72
|
-
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
73
|
-
this.logger.error(`Errore processing richiesta: ${msg}`);
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
async processSuspiciousRequest(req) {
|
|
51
|
+
handleSuspicious(req) {
|
|
77
52
|
const ip = this.getClientIp(req);
|
|
78
|
-
if (ip
|
|
79
|
-
return;
|
|
80
|
-
if (this.isThrottled(ip))
|
|
53
|
+
if (!ip || this.isThrottled(ip))
|
|
81
54
|
return;
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
this.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
55
|
+
const entry = {
|
|
56
|
+
timestamp: new Date().toISOString(),
|
|
57
|
+
type: "ATTACK",
|
|
58
|
+
ip,
|
|
59
|
+
method: req.method,
|
|
60
|
+
url: this.sanitize(req.url),
|
|
61
|
+
};
|
|
62
|
+
const line = JSON.stringify(entry) + "\n";
|
|
63
|
+
if (!this.stream.write(line)) {
|
|
64
|
+
this.stream.once("drain", () => {
|
|
65
|
+
this.logger.debug("Stream del log degli attacchi svuotato");
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
this.attSrv.updateIpList(ip).catch((err) => {
|
|
69
|
+
const msg = err instanceof Error ? err.message : "Errore sconosciuto";
|
|
70
|
+
;
|
|
71
|
+
this.logger.error(`Aggiornamento Cloudflare fallito: ${msg}`);
|
|
72
|
+
});
|
|
96
73
|
}
|
|
97
74
|
isThrottled(ip) {
|
|
98
|
-
|
|
99
|
-
const last = this.recentIps.get(ip);
|
|
100
|
-
if (last && now - last < this.THROTTLE_MS)
|
|
75
|
+
if (this.recentIps.has(ip))
|
|
101
76
|
return true;
|
|
102
|
-
|
|
103
|
-
this.recentIps.
|
|
104
|
-
this.
|
|
77
|
+
const timeout = setTimeout(() => {
|
|
78
|
+
this.recentIps.delete(ip);
|
|
79
|
+
}, this.throttleMs);
|
|
80
|
+
this.recentIps.set(ip, timeout);
|
|
105
81
|
return false;
|
|
106
82
|
}
|
|
107
83
|
getClientIp(req) {
|
|
@@ -115,11 +91,29 @@ let AttackLoggerMiddleware = class AttackLoggerMiddleware {
|
|
|
115
91
|
if (typeof xForwardedFor === "string") {
|
|
116
92
|
return xForwardedFor.split(",")[0].trim();
|
|
117
93
|
}
|
|
118
|
-
return req.socket.remoteAddress ??
|
|
94
|
+
return req.socket.remoteAddress ?? null;
|
|
95
|
+
}
|
|
96
|
+
sanitize(value) {
|
|
97
|
+
return value.replace(/[\r\n]/g, "_");
|
|
98
|
+
}
|
|
99
|
+
ensureDirectoryExists(filePath) {
|
|
100
|
+
const dir = (0, path_1.dirname)(filePath);
|
|
101
|
+
if (!(0, fs_1.existsSync)(dir)) {
|
|
102
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
103
|
+
this.logger.log(`Cartella dei log creata: ${dir}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
onModuleDestroy() {
|
|
107
|
+
this.stream.end();
|
|
108
|
+
for (const timeout of this.recentIps.values()) {
|
|
109
|
+
clearTimeout(timeout);
|
|
110
|
+
}
|
|
111
|
+
this.recentIps.clear();
|
|
119
112
|
}
|
|
120
113
|
};
|
|
121
114
|
exports.AttackLoggerMiddleware = AttackLoggerMiddleware;
|
|
122
|
-
exports.AttackLoggerMiddleware = AttackLoggerMiddleware = __decorate([
|
|
115
|
+
exports.AttackLoggerMiddleware = AttackLoggerMiddleware = AttackLoggerMiddleware_1 = __decorate([
|
|
123
116
|
(0, common_1.Injectable)(),
|
|
124
|
-
|
|
117
|
+
__param(1, (0, common_1.Inject)(utils_1.CLOUDFLARE_OPTIONS)),
|
|
118
|
+
__metadata("design:paramtypes", [attacks_service_1.AttacksService, Object])
|
|
125
119
|
], AttackLoggerMiddleware);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CLOUDFLARE_OPTIONS: unique symbol;
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
|
@@ -14,4 +14,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./cloudflare.constants"), exports);
|
|
17
18
|
__exportStar(require("./get-client-ip.util"), exports);
|
|
19
|
+
__exportStar(require("./options.validator"), exports);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateOptions = validateOptions;
|
|
4
|
+
function validateOptions(opzioni) {
|
|
5
|
+
if (!opzioni)
|
|
6
|
+
throw new Error("CloudflareAttacksOptions è obbligatorio");
|
|
7
|
+
const campiObbligatori = ["apiToken", "accountId", "listId", "comment", "logPath"];
|
|
8
|
+
for (const campo of campiObbligatori) {
|
|
9
|
+
const valore = opzioni[campo];
|
|
10
|
+
if (typeof valore !== "string" || valore.trim().length === 0) {
|
|
11
|
+
throw new Error(`CloudflareAttacksOptions.${campo} deve essere una stringa non vuota`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|