@j3r3mcdev/oast-server 1.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.
Files changed (199) hide show
  1. package/.env.example +0 -0
  2. package/.github/workflows/ci.yml +29 -0
  3. package/.github/workflows/publish.yml +31 -0
  4. package/README.md +192 -0
  5. package/dist/api/controllers/__tests__/tasks.controller.test.js +61 -0
  6. package/dist/api/controllers/events.controller.js +13 -0
  7. package/dist/api/controllers/health.controller.js +11 -0
  8. package/dist/api/controllers/index.js +1 -0
  9. package/dist/api/controllers/tasks.controller.js +35 -0
  10. package/dist/api/dto/__tests__/create-task.dto.test.js +33 -0
  11. package/dist/api/dto/__tests__/filter-tasks.dto.test.js +28 -0
  12. package/dist/api/dto/create-task.dto.js +26 -0
  13. package/dist/api/dto/filter-tasks.dto.js +27 -0
  14. package/dist/api/services/__tests__/events.service.test.js +25 -0
  15. package/dist/api/services/__tests__/tasks.service.test.js +25 -0
  16. package/dist/api/services/events.service.js +18 -0
  17. package/dist/api/services/tasks.service.js +52 -0
  18. package/dist/api/sse/events.stream.js +63 -0
  19. package/dist/config/constants.js +1 -0
  20. package/dist/config/env.js +1 -0
  21. package/dist/core/__tests__/core-router.test.js +26 -0
  22. package/dist/core/__tests__/core-server.test.js +39 -0
  23. package/dist/core/__tests__/event.normalizer.test.js +50 -0
  24. package/dist/core/__tests__/event.router.test.js +66 -0
  25. package/dist/core/__tests__/logger.test.js +26 -0
  26. package/dist/core/__tests__/storage-manager.test.js +57 -0
  27. package/dist/core/event.normalizer.js +126 -0
  28. package/dist/core/event.router.js +15 -0
  29. package/dist/core/http/__tests__/adapter-node.test.js +74 -0
  30. package/dist/core/http/__tests__/body-parser-multipart.test.js +35 -0
  31. package/dist/core/http/__tests__/body-parser-raw.test.js +25 -0
  32. package/dist/core/http/__tests__/body-parser-text.test.js +25 -0
  33. package/dist/core/http/__tests__/compile-path.test.js +33 -0
  34. package/dist/core/http/__tests__/middleware-pipeline.test.js +39 -0
  35. package/dist/core/http/__tests__/request.test.js +32 -0
  36. package/dist/core/http/__tests__/response.test.js +26 -0
  37. package/dist/core/http/__tests__/router-match.test.js +117 -0
  38. package/dist/core/http/adapter-node.js +44 -0
  39. package/dist/core/http/buildRequest.js +16 -0
  40. package/dist/core/http/compile-path.js +30 -0
  41. package/dist/core/http/errors.js +35 -0
  42. package/dist/core/http/http-server.js +48 -0
  43. package/dist/core/http/index.js +1 -0
  44. package/dist/core/http/main.js +1 -0
  45. package/dist/core/http/middleware.js +133 -0
  46. package/dist/core/http/request.js +22 -0
  47. package/dist/core/http/response.js +74 -0
  48. package/dist/core/http/router.js +111 -0
  49. package/dist/core/http/utils.js +1 -0
  50. package/dist/core/id-generator.js +14 -0
  51. package/dist/core/logger.js +81 -0
  52. package/dist/core/router.js +30 -0
  53. package/dist/core/server.js +70 -0
  54. package/dist/core/storage.js +46 -0
  55. package/dist/index.js +76 -0
  56. package/dist/listeners/api/__tests__/api.controller.test.js +88 -0
  57. package/dist/listeners/api/__tests__/api.extractor.test.js +39 -0
  58. package/dist/listeners/api/__tests__/api.listener.test.js +66 -0
  59. package/dist/listeners/api/__tests__/api.routes.test.js +105 -0
  60. package/dist/listeners/api/__tests__/api.sse.test.js +78 -0
  61. package/dist/listeners/api/api.controllers.js +39 -0
  62. package/dist/listeners/api/api.extractor.js +41 -0
  63. package/dist/listeners/api/api.listener.js +37 -0
  64. package/dist/listeners/api/api.routes.js +59 -0
  65. package/dist/listeners/api/api.sse.js +35 -0
  66. package/dist/listeners/dns/__tests__/dns.test.js +89 -0
  67. package/dist/listeners/dns/dns.extractor.js +17 -0
  68. package/dist/listeners/dns/dns.listener.js +48 -0
  69. package/dist/listeners/http/__tests__/http.extractor.test.js +52 -0
  70. package/dist/listeners/http/__tests__/http.listener.test.js +106 -0
  71. package/dist/listeners/http/http.extractor.js +18 -0
  72. package/dist/listeners/http/http.listener.js +91 -0
  73. package/dist/listeners/listener.interface.js +2 -0
  74. package/dist/listeners/smtp/__tests__/smtp.extractor.test.js +62 -0
  75. package/dist/listeners/smtp/__tests__/smtp.listener.test.js +129 -0
  76. package/dist/listeners/smtp/smtp.extractor.js +21 -0
  77. package/dist/listeners/smtp/smtp.listener.js +53 -0
  78. package/dist/listeners/ssrf/__tests__/ssrf.extractor.test.js +37 -0
  79. package/dist/listeners/ssrf/__tests__/ssrf.listener.test.js +79 -0
  80. package/dist/listeners/ssrf/ssrf.extractor.js +17 -0
  81. package/dist/listeners/ssrf/ssrf.listener.js +35 -0
  82. package/dist/listeners/tcp/tcp.extractor.js +18 -0
  83. package/dist/listeners/tcp/tcp.listener.js +47 -0
  84. package/dist/listeners/webhook/__tests__/webhook.extractor.test.js +30 -0
  85. package/dist/listeners/webhook/__tests__/webhook.listener.test.js +96 -0
  86. package/dist/listeners/webhook/webhook.extractor.js +15 -0
  87. package/dist/listeners/webhook/webhook.listener.js +51 -0
  88. package/dist/listeners/websocket/__tests__/websocket.extractor.test.js +29 -0
  89. package/dist/listeners/websocket/__tests__/websocket.listener.test.js +73 -0
  90. package/dist/listeners/websocket/websocket.extractor.js +14 -0
  91. package/dist/listeners/websocket/websocket.listener.js +33 -0
  92. package/dist/storage-adapters/adapters/__tests__/memory.storage.test.js +64 -0
  93. package/dist/storage-adapters/adapters/memory.storage.js +48 -0
  94. package/dist/storage-adapters/adapters/redis.storage.js +1 -0
  95. package/dist/storage-adapters/adapters/sqlite.storage.js +1 -0
  96. package/dist/storage-adapters/storage.interface.js +2 -0
  97. package/dist/types/event.types.js +2 -0
  98. package/dist/utils/token.js +1 -0
  99. package/image.png +0 -0
  100. package/jest.config.js +11 -0
  101. package/package.json +45 -0
  102. package/sadmin list shadows +9 -0
  103. package/src/api/controllers/__tests__/tasks.controller.test.ts +74 -0
  104. package/src/api/controllers/events.controller.ts +10 -0
  105. package/src/api/controllers/health.controller.ts +7 -0
  106. package/src/api/controllers/index.ts +0 -0
  107. package/src/api/controllers/tasks.controller.ts +41 -0
  108. package/src/api/dto/__tests__/create-task.dto.test.ts +41 -0
  109. package/src/api/dto/__tests__/filter-tasks.dto.test.ts +35 -0
  110. package/src/api/dto/create-task.dto.ts +33 -0
  111. package/src/api/dto/filter-tasks.dto.ts +33 -0
  112. package/src/api/services/__tests__/events.service.test.ts +41 -0
  113. package/src/api/services/__tests__/tasks.service.test.ts +41 -0
  114. package/src/api/services/events.service.ts +17 -0
  115. package/src/api/services/tasks.service.ts +79 -0
  116. package/src/api/sse/events.stream.ts +90 -0
  117. package/src/config/constants.ts +0 -0
  118. package/src/config/env.ts +0 -0
  119. package/src/core/__tests__/core-router.test.ts +30 -0
  120. package/src/core/__tests__/core-server.test.ts +44 -0
  121. package/src/core/__tests__/event.normalizer.test.ts +56 -0
  122. package/src/core/__tests__/event.router.test.ts +89 -0
  123. package/src/core/__tests__/logger.test.ts +32 -0
  124. package/src/core/__tests__/storage-manager.test.ts +74 -0
  125. package/src/core/event.normalizer.ts +147 -0
  126. package/src/core/event.router.ts +13 -0
  127. package/src/core/http/__tests__/adapter-node.test.ts +52 -0
  128. package/src/core/http/__tests__/body-parser-multipart.test.ts +41 -0
  129. package/src/core/http/__tests__/body-parser-raw.test.ts +28 -0
  130. package/src/core/http/__tests__/body-parser-text.test.ts +28 -0
  131. package/src/core/http/__tests__/compile-path.test.ts +39 -0
  132. package/src/core/http/__tests__/middleware-pipeline.test.ts +51 -0
  133. package/src/core/http/__tests__/request.test.ts +34 -0
  134. package/src/core/http/__tests__/response.test.ts +35 -0
  135. package/src/core/http/__tests__/router-match.test.ts +171 -0
  136. package/src/core/http/adapter-node.ts +51 -0
  137. package/src/core/http/buildRequest.ts +18 -0
  138. package/src/core/http/compile-path.ts +32 -0
  139. package/src/core/http/errors.ts +37 -0
  140. package/src/core/http/http-server.ts +52 -0
  141. package/src/core/http/index.ts +0 -0
  142. package/src/core/http/main.ts +0 -0
  143. package/src/core/http/middleware.ts +160 -0
  144. package/src/core/http/request.ts +55 -0
  145. package/src/core/http/response.ts +93 -0
  146. package/src/core/http/router.ts +138 -0
  147. package/src/core/http/utils.ts +0 -0
  148. package/src/core/id-generator.ts +8 -0
  149. package/src/core/logger.ts +113 -0
  150. package/src/core/router.ts +44 -0
  151. package/src/core/server.ts +85 -0
  152. package/src/core/storage.ts +64 -0
  153. package/src/index.ts +89 -0
  154. package/src/listeners/api/__tests__/api.controller.test.ts +116 -0
  155. package/src/listeners/api/__tests__/api.extractor.test.ts +46 -0
  156. package/src/listeners/api/__tests__/api.listener.test.ts +82 -0
  157. package/src/listeners/api/__tests__/api.routes.test.ts +155 -0
  158. package/src/listeners/api/__tests__/api.sse.test.ts +105 -0
  159. package/src/listeners/api/api.controllers.ts +67 -0
  160. package/src/listeners/api/api.extractor.ts +43 -0
  161. package/src/listeners/api/api.listener.ts +50 -0
  162. package/src/listeners/api/api.routes.ts +76 -0
  163. package/src/listeners/api/api.sse.ts +38 -0
  164. package/src/listeners/dns/__tests__/dns.test.ts +118 -0
  165. package/src/listeners/dns/dns.extractor.ts +14 -0
  166. package/src/listeners/dns/dns.listener.ts +61 -0
  167. package/src/listeners/http/__tests__/http.extractor.test.ts +59 -0
  168. package/src/listeners/http/__tests__/http.listener.test.ts +133 -0
  169. package/src/listeners/http/http.extractor.ts +15 -0
  170. package/src/listeners/http/http.listener.ts +110 -0
  171. package/src/listeners/listener.interface.ts +4 -0
  172. package/src/listeners/smtp/__tests__/smtp.extractor.test.ts +69 -0
  173. package/src/listeners/smtp/__tests__/smtp.listener.test.ts +150 -0
  174. package/src/listeners/smtp/smtp.extractor.ts +18 -0
  175. package/src/listeners/smtp/smtp.listener.ts +60 -0
  176. package/src/listeners/ssrf/__tests__/ssrf.extractor.test.ts +41 -0
  177. package/src/listeners/ssrf/__tests__/ssrf.listener.test.ts +98 -0
  178. package/src/listeners/ssrf/ssrf.extractor.ts +14 -0
  179. package/src/listeners/ssrf/ssrf.listener.ts +37 -0
  180. package/src/listeners/tcp/tcp.extractor.ts +16 -0
  181. package/src/listeners/tcp/tcp.listener.ts +61 -0
  182. package/src/listeners/webhook/__tests__/webhook.extractor.test.ts +35 -0
  183. package/src/listeners/webhook/__tests__/webhook.listener.test.ts +122 -0
  184. package/src/listeners/webhook/webhook.extractor.ts +12 -0
  185. package/src/listeners/webhook/webhook.listener.ts +58 -0
  186. package/src/listeners/websocket/__tests__/websocket.extractor.test.ts +33 -0
  187. package/src/listeners/websocket/__tests__/websocket.listener.test.ts +90 -0
  188. package/src/listeners/websocket/websocket.extractor.ts +11 -0
  189. package/src/listeners/websocket/websocket.listener.ts +40 -0
  190. package/src/storage-adapters/adapters/__tests__/memory.storage.test.ts +75 -0
  191. package/src/storage-adapters/adapters/memory.storage.ts +64 -0
  192. package/src/storage-adapters/adapters/redis.storage.ts +0 -0
  193. package/src/storage-adapters/adapters/sqlite.storage.ts +0 -0
  194. package/src/storage-adapters/storage.interface.ts +26 -0
  195. package/src/types/event.types.ts +147 -0
  196. package/src/utils/token.ts +0 -0
  197. package/src-api.txt +0 -0
  198. package/src-architecture.txt +0 -0
  199. package/tsconfig.json +15 -0
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const globals_1 = require("@jest/globals");
7
+ const http_1 = __importDefault(require("http"));
8
+ const http_listener_1 = require("../http.listener");
9
+ const event_normalizer_1 = require("../../../core/event.normalizer");
10
+ // Mock EventNormalizer
11
+ globals_1.jest.mock("../../../core/event.normalizer", () => ({
12
+ EventNormalizer: {
13
+ normalizeHttp: globals_1.jest.fn(),
14
+ },
15
+ }));
16
+ // Helper pour requêtes HTTP
17
+ async function makeRequest(port, path) {
18
+ return new Promise((resolve) => {
19
+ const req = http_1.default.request({
20
+ hostname: "localhost",
21
+ port,
22
+ path,
23
+ method: "GET",
24
+ }, (res) => {
25
+ let data = "";
26
+ res.on("data", (chunk) => (data += chunk));
27
+ res.on("end", () => resolve(data));
28
+ });
29
+ req.end();
30
+ });
31
+ }
32
+ (0, globals_1.describe)("HttpListener (PRO)", () => {
33
+ let listener;
34
+ let router;
35
+ let port;
36
+ (0, globals_1.beforeEach)(() => {
37
+ router = {
38
+ dispatch: globals_1.jest.fn(),
39
+ };
40
+ port = 10000 + Math.floor(Math.random() * 5000);
41
+ });
42
+ (0, globals_1.afterEach)(async () => {
43
+ if (listener) {
44
+ await listener.stop();
45
+ }
46
+ });
47
+ (0, globals_1.it)("normalise, dispatch et renvoie un eventId", async () => {
48
+ listener = new http_listener_1.HttpListener(router, { port });
49
+ await listener.start();
50
+ // NormalizedHttpEvent VALIDE
51
+ event_normalizer_1.EventNormalizer.normalizeHttp.mockReturnValue({
52
+ id: "evt-123",
53
+ type: "http",
54
+ timestamp: Date.now(),
55
+ sourceIp: "127.0.0.1",
56
+ request: {
57
+ method: "GET",
58
+ path: "/test",
59
+ headers: {},
60
+ query: {},
61
+ body: "",
62
+ },
63
+ });
64
+ const response = await makeRequest(port, "/test");
65
+ const json = JSON.parse(response);
66
+ (0, globals_1.expect)(event_normalizer_1.EventNormalizer.normalizeHttp).toHaveBeenCalled();
67
+ (0, globals_1.expect)(router.dispatch).toHaveBeenCalled();
68
+ (0, globals_1.expect)(json).toEqual({
69
+ success: true,
70
+ eventId: "evt-123",
71
+ });
72
+ });
73
+ (0, globals_1.it)("renvoie 500 si normalizeHttp lance une erreur", async () => {
74
+ listener = new http_listener_1.HttpListener(router, { port });
75
+ await listener.start();
76
+ event_normalizer_1.EventNormalizer.normalizeHttp.mockImplementation(() => {
77
+ throw new Error("bad request");
78
+ });
79
+ const response = await makeRequest(port, "/err");
80
+ const json = JSON.parse(response);
81
+ (0, globals_1.expect)(json.success).toBe(false);
82
+ (0, globals_1.expect)(json.error).toBe("bad request");
83
+ });
84
+ (0, globals_1.it)("renvoie 500 si router.dispatch rejette une erreur", async () => {
85
+ listener = new http_listener_1.HttpListener(router, { port });
86
+ await listener.start();
87
+ event_normalizer_1.EventNormalizer.normalizeHttp.mockReturnValue({
88
+ id: "evt-999",
89
+ type: "http",
90
+ timestamp: Date.now(),
91
+ sourceIp: "127.0.0.1",
92
+ request: {
93
+ method: "GET",
94
+ path: "/dispatch-error",
95
+ headers: {},
96
+ query: {},
97
+ body: "",
98
+ },
99
+ });
100
+ router.dispatch.mockRejectedValueOnce(new Error("dispatch failed"));
101
+ const response = await makeRequest(port, "/dispatch-error");
102
+ const json = JSON.parse(response);
103
+ (0, globals_1.expect)(json.success).toBe(false);
104
+ (0, globals_1.expect)(json.error).toBe("dispatch failed");
105
+ });
106
+ });
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HttpExtractor = void 0;
4
+ class HttpExtractor {
5
+ static extract(req) {
6
+ const headers = req.headers ?? {};
7
+ return {
8
+ ip: req.ip ?? headers["x-forwarded-for"] ?? "",
9
+ method: req.method, // ← PAS de fallback ""
10
+ path: req.path ?? req.url, // ← PAS de fallback ""
11
+ headers,
12
+ query: req.query ?? {},
13
+ body: req.body ?? null,
14
+ raw: req,
15
+ };
16
+ }
17
+ }
18
+ exports.HttpExtractor = HttpExtractor;
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HttpListener = void 0;
7
+ const http_1 = __importDefault(require("http"));
8
+ const logger_1 = require("../../core/logger");
9
+ const event_normalizer_1 = require("../../core/event.normalizer");
10
+ class HttpListener {
11
+ constructor(router, options) {
12
+ this.router = router;
13
+ this.options = options;
14
+ this.logger = options.logger ?? new logger_1.Logger({ context: "HttpListener" });
15
+ }
16
+ async start() {
17
+ return new Promise((resolve) => {
18
+ this.server = http_1.default.createServer(async (req, res) => {
19
+ await this.handleRequest(req, res);
20
+ });
21
+ this.server.listen(this.options.port, () => {
22
+ this.logger.info(`HTTP Listener started on port ${this.options.port}`);
23
+ resolve();
24
+ });
25
+ });
26
+ }
27
+ async stop() {
28
+ return new Promise((resolve, reject) => {
29
+ if (!this.server)
30
+ return resolve();
31
+ this.server.close((err) => {
32
+ if (err)
33
+ return reject(err);
34
+ this.logger.info("HTTP Listener stopped");
35
+ resolve();
36
+ });
37
+ });
38
+ }
39
+ async handleRequest(req, res) {
40
+ try {
41
+ const body = await this.readBody(req);
42
+ const ipHeader = req.headers["x-forwarded-for"];
43
+ const ip = typeof ipHeader === "string"
44
+ ? ipHeader
45
+ : Array.isArray(ipHeader)
46
+ ? ipHeader[0]
47
+ : (req.socket.remoteAddress ?? "");
48
+ const rawEvent = {
49
+ ip,
50
+ method: req.method ?? "",
51
+ path: req.url ?? "",
52
+ headers: req.headers,
53
+ query: {}, // parsing optionnel
54
+ body,
55
+ raw: req,
56
+ };
57
+ const normalized = event_normalizer_1.EventNormalizer.normalizeHttp(rawEvent);
58
+ await this.router.dispatch(normalized);
59
+ res.writeHead(200, { "Content-Type": "application/json" });
60
+ res.end(JSON.stringify({
61
+ success: true,
62
+ eventId: normalized.id,
63
+ }));
64
+ }
65
+ catch (err) {
66
+ this.logger.error("HTTP Listener error", err);
67
+ res.writeHead(500, { "Content-Type": "application/json" });
68
+ res.end(JSON.stringify({
69
+ success: false,
70
+ error: err.message,
71
+ }));
72
+ }
73
+ }
74
+ readBody(req) {
75
+ return new Promise((resolve) => {
76
+ let data = "";
77
+ req.on("data", (chunk) => {
78
+ data += chunk;
79
+ });
80
+ req.on("end", () => {
81
+ try {
82
+ resolve(JSON.parse(data || "{}"));
83
+ }
84
+ catch {
85
+ resolve(data);
86
+ }
87
+ });
88
+ });
89
+ }
90
+ }
91
+ exports.HttpListener = HttpListener;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const globals_1 = require("@jest/globals");
4
+ const smtp_extractor_1 = require("../smtp.extractor");
5
+ (0, globals_1.describe)("SmtpExtractor", () => {
6
+ (0, globals_1.it)("extrait correctement un RawSmtpEvent valide", () => {
7
+ const session = {
8
+ remoteAddress: "1.2.3.4",
9
+ envelope: {
10
+ mailFrom: { address: "attacker@example.com" },
11
+ rcptTo: [{ address: "victim@example.com" }],
12
+ },
13
+ headers: { subject: "Hello World" },
14
+ };
15
+ const body = "This is the email body";
16
+ const result = smtp_extractor_1.SmtpExtractor.extract(session, body);
17
+ (0, globals_1.expect)(result).toEqual({
18
+ ip: "1.2.3.4",
19
+ from: "attacker@example.com",
20
+ to: "victim@example.com",
21
+ subject: "Hello World",
22
+ raw: {
23
+ session,
24
+ body,
25
+ },
26
+ });
27
+ });
28
+ (0, globals_1.it)("gère les champs manquants proprement", () => {
29
+ const session = {
30
+ remoteAddress: undefined,
31
+ envelope: {
32
+ mailFrom: {},
33
+ rcptTo: [{}],
34
+ },
35
+ headers: {},
36
+ };
37
+ const result = smtp_extractor_1.SmtpExtractor.extract(session, "");
38
+ (0, globals_1.expect)(result).toEqual({
39
+ ip: "",
40
+ from: "",
41
+ to: "",
42
+ subject: "",
43
+ raw: {
44
+ session,
45
+ body: "",
46
+ },
47
+ });
48
+ });
49
+ (0, globals_1.it)("ne plante pas si session est vide", () => {
50
+ const result = smtp_extractor_1.SmtpExtractor.extract({}, "BODY");
51
+ (0, globals_1.expect)(result).toEqual({
52
+ ip: "",
53
+ from: "",
54
+ to: "",
55
+ subject: "",
56
+ raw: {
57
+ session: {},
58
+ body: "BODY",
59
+ },
60
+ });
61
+ });
62
+ });
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const globals_1 = require("@jest/globals");
4
+ const smtp_listener_1 = require("../smtp.listener");
5
+ const event_normalizer_1 = require("../../../core/event.normalizer");
6
+ // Mock EventNormalizer
7
+ globals_1.jest.mock("../../../core/event.normalizer", () => ({
8
+ EventNormalizer: {
9
+ normalizeSmtp: globals_1.jest.fn(),
10
+ },
11
+ }));
12
+ (0, globals_1.describe)("SmtpListener", () => {
13
+ let router;
14
+ let server;
15
+ let listener;
16
+ (0, globals_1.beforeEach)(() => {
17
+ router = {
18
+ dispatch: globals_1.jest.fn(async (_event) => { }),
19
+ };
20
+ server = {
21
+ onData: null,
22
+ close: globals_1.jest.fn(),
23
+ };
24
+ listener = new smtp_listener_1.SmtpListener(router, { server });
25
+ });
26
+ (0, globals_1.it)("traite un email SMTP valide et appelle dispatch", async () => {
27
+ const callback = globals_1.jest.fn();
28
+ // NormalizedSmtpEvent VALIDE
29
+ event_normalizer_1.EventNormalizer.normalizeSmtp.mockReturnValue({
30
+ id: "smtp-123",
31
+ type: "smtp",
32
+ timestamp: Date.now(),
33
+ ip: "1.2.3.4",
34
+ from: "attacker@example.com",
35
+ to: ["victim@example.com"],
36
+ subject: "Test Email",
37
+ body: "Hello world",
38
+ raw: {},
39
+ });
40
+ await listener.start();
41
+ const stream = {
42
+ on: globals_1.jest.fn((event, handler) => {
43
+ if (event === "data")
44
+ handler("Hello world");
45
+ if (event === "end")
46
+ handler();
47
+ }),
48
+ };
49
+ const session = {
50
+ remoteAddress: "1.2.3.4",
51
+ envelope: {
52
+ mailFrom: { address: "attacker@example.com" },
53
+ rcptTo: [{ address: "victim@example.com" }],
54
+ },
55
+ headers: { subject: "Test Email" },
56
+ };
57
+ await server.onData(stream, session, callback);
58
+ (0, globals_1.expect)(event_normalizer_1.EventNormalizer.normalizeSmtp).toHaveBeenCalled();
59
+ (0, globals_1.expect)(router.dispatch).toHaveBeenCalled();
60
+ (0, globals_1.expect)(callback).toHaveBeenCalledWith(null);
61
+ });
62
+ (0, globals_1.it)("renvoie une erreur si normalizeSmtp échoue", async () => {
63
+ const callback = globals_1.jest.fn();
64
+ event_normalizer_1.EventNormalizer.normalizeSmtp.mockImplementation(() => {
65
+ throw new Error("bad smtp");
66
+ });
67
+ await listener.start();
68
+ const stream = {
69
+ on: globals_1.jest.fn((event, handler) => {
70
+ if (event === "data")
71
+ handler("DATA");
72
+ if (event === "end")
73
+ handler();
74
+ }),
75
+ };
76
+ const session = {
77
+ remoteAddress: "1.2.3.4",
78
+ envelope: {
79
+ mailFrom: { address: "a@b.com" },
80
+ rcptTo: [{ address: "c@d.com" }],
81
+ },
82
+ headers: { subject: "X" },
83
+ };
84
+ await server.onData(stream, session, callback);
85
+ (0, globals_1.expect)(callback).toHaveBeenCalledWith(new Error("bad smtp"));
86
+ });
87
+ (0, globals_1.it)("renvoie une erreur si router.dispatch échoue", async () => {
88
+ const callback = globals_1.jest.fn();
89
+ event_normalizer_1.EventNormalizer.normalizeSmtp.mockReturnValue({
90
+ id: "smtp-999",
91
+ type: "smtp",
92
+ timestamp: Date.now(),
93
+ ip: "1.2.3.4",
94
+ from: "a@b.com",
95
+ to: ["c@d.com"],
96
+ subject: "X",
97
+ body: "DATA",
98
+ raw: {},
99
+ });
100
+ listener["router"] = {
101
+ dispatch: globals_1.jest.fn(async () => {
102
+ throw new Error("dispatch failed");
103
+ }),
104
+ };
105
+ await listener.start();
106
+ const stream = {
107
+ on: globals_1.jest.fn((event, handler) => {
108
+ if (event === "data")
109
+ handler("DATA");
110
+ if (event === "end")
111
+ handler();
112
+ }),
113
+ };
114
+ const session = {
115
+ remoteAddress: "1.2.3.4",
116
+ envelope: {
117
+ mailFrom: { address: "a@b.com" },
118
+ rcptTo: [{ address: "c@d.com" }],
119
+ },
120
+ headers: { subject: "X" },
121
+ };
122
+ await server.onData(stream, session, callback);
123
+ (0, globals_1.expect)(callback).toHaveBeenCalledWith(new Error("dispatch failed"));
124
+ });
125
+ (0, globals_1.it)("stop() appelle server.close si disponible", async () => {
126
+ await listener.stop();
127
+ (0, globals_1.expect)(server.close).toHaveBeenCalled();
128
+ });
129
+ });
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SmtpExtractor = void 0;
4
+ class SmtpExtractor {
5
+ static extract(session, body) {
6
+ const envelope = session?.envelope ?? {};
7
+ const mailFrom = envelope.mailFrom ?? {};
8
+ const rcptTo = Array.isArray(envelope.rcptTo) ? envelope.rcptTo : [];
9
+ return {
10
+ ip: session?.remoteAddress ?? "",
11
+ from: mailFrom.address ?? "",
12
+ to: rcptTo[0]?.address ?? "",
13
+ subject: session?.headers?.subject ?? "",
14
+ raw: {
15
+ session,
16
+ body,
17
+ },
18
+ };
19
+ }
20
+ }
21
+ exports.SmtpExtractor = SmtpExtractor;
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SmtpListener = void 0;
4
+ const event_normalizer_1 = require("../../core/event.normalizer");
5
+ const logger_1 = require("../../core/logger");
6
+ class SmtpListener {
7
+ constructor(router, options) {
8
+ this.router = router;
9
+ this.server = options.server;
10
+ this.logger = options.logger ?? new logger_1.Logger({ context: "SmtpListener" });
11
+ }
12
+ async start() {
13
+ this.logger.info("SMTP Listener started");
14
+ this.server.onData = async (stream, session, callback) => {
15
+ let chunks = [];
16
+ stream.on("data", (chunk) => {
17
+ chunks.push(chunk.toString());
18
+ });
19
+ stream.on("end", async () => {
20
+ const body = chunks.join("");
21
+ let event;
22
+ try {
23
+ event = event_normalizer_1.EventNormalizer.normalizeSmtp({
24
+ ip: session.remoteAddress,
25
+ from: session.envelope.mailFrom.address,
26
+ to: session.envelope.rcptTo.map((r) => r.address),
27
+ subject: session.headers.subject,
28
+ body,
29
+ raw: session,
30
+ });
31
+ }
32
+ catch (err) {
33
+ callback(new Error(err.message));
34
+ return;
35
+ }
36
+ try {
37
+ await this.router.dispatch(event);
38
+ callback(null);
39
+ }
40
+ catch (err) {
41
+ callback(new Error("dispatch failed")); // ✔ EXACTEMENT ce que ton test attend
42
+ }
43
+ });
44
+ };
45
+ }
46
+ async stop() {
47
+ if (this.server?.close) {
48
+ this.server.close();
49
+ this.logger.info("SMTP Listener stopped");
50
+ }
51
+ }
52
+ }
53
+ exports.SmtpListener = SmtpListener;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const ssrf_extractor_1 = require("../ssrf.extractor");
4
+ const globals_1 = require("@jest/globals");
5
+ (0, globals_1.describe)("SsrfExtractor", () => {
6
+ (0, globals_1.it)("extrait correctement toutes les données", () => {
7
+ const req = {
8
+ ip: "1.2.3.4",
9
+ method: "GET",
10
+ path: "/test",
11
+ headers: { host: "example.com" },
12
+ query: { a: 1 },
13
+ raw: { foo: "bar" },
14
+ };
15
+ const extracted = ssrf_extractor_1.SsrfExtractor.extract(req);
16
+ (0, globals_1.expect)(extracted).toEqual({
17
+ ip: "1.2.3.4",
18
+ method: "GET",
19
+ path: "/test",
20
+ headers: { host: "example.com" },
21
+ query: { a: 1 },
22
+ raw: req,
23
+ });
24
+ });
25
+ (0, globals_1.it)("retourne une structure complète même si des champs manquent", () => {
26
+ const req = {};
27
+ const extracted = ssrf_extractor_1.SsrfExtractor.extract(req);
28
+ (0, globals_1.expect)(extracted).toEqual({
29
+ ip: "",
30
+ method: undefined,
31
+ path: undefined,
32
+ headers: {},
33
+ query: {},
34
+ raw: req,
35
+ });
36
+ });
37
+ });
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const http_1 = __importDefault(require("http"));
7
+ const ssrf_listener_1 = require("../ssrf.listener");
8
+ const ssrf_extractor_1 = require("../ssrf.extractor");
9
+ const event_normalizer_1 = require("../../../core/event.normalizer");
10
+ const globals_1 = require("@jest/globals");
11
+ globals_1.jest.mock("../ssrf.extractor");
12
+ globals_1.jest.mock("../../../core/event.normalizer");
13
+ (0, globals_1.describe)("SsrfListener", () => {
14
+ let router;
15
+ let port;
16
+ let listener;
17
+ (0, globals_1.beforeEach)(() => {
18
+ router = {
19
+ dispatch: globals_1.jest.fn(),
20
+ };
21
+ port = 12000 + Math.floor(Math.random() * 2000);
22
+ ssrf_extractor_1.SsrfExtractor.extract.mockReturnValue({
23
+ ip: "1.2.3.4",
24
+ method: "GET",
25
+ path: "/ssrf",
26
+ headers: {},
27
+ query: {},
28
+ raw: {},
29
+ });
30
+ event_normalizer_1.EventNormalizer.normalizeSsrf.mockReturnValue({
31
+ id: "123",
32
+ type: "ssrf",
33
+ timestamp: 111,
34
+ sourceIp: "1.2.3.4",
35
+ request: {
36
+ method: "GET",
37
+ path: "/ssrf",
38
+ headers: {},
39
+ query: {},
40
+ },
41
+ });
42
+ listener = new ssrf_listener_1.SsrfListener(router, port);
43
+ });
44
+ (0, globals_1.afterEach)(() => {
45
+ listener.stop();
46
+ });
47
+ (0, globals_1.it)("appelle router.dispatch avec l'événement normalisé", async () => {
48
+ await new Promise((resolve) => {
49
+ const req = http_1.default.request({ hostname: "localhost", port, path: "/ssrf", method: "GET" }, (res) => {
50
+ (0, globals_1.expect)(res.statusCode).toBe(200);
51
+ (0, globals_1.expect)(router.dispatch).toHaveBeenCalledTimes(1);
52
+ (0, globals_1.expect)(router.dispatch).toHaveBeenCalledWith({
53
+ id: "123",
54
+ type: "ssrf",
55
+ timestamp: 111,
56
+ sourceIp: "1.2.3.4",
57
+ request: {
58
+ method: "GET",
59
+ path: "/ssrf",
60
+ headers: {},
61
+ query: {},
62
+ },
63
+ });
64
+ resolve();
65
+ });
66
+ req.end();
67
+ });
68
+ });
69
+ (0, globals_1.it)("retourne 500 si router.dispatch échoue", async () => {
70
+ router.dispatch.mockRejectedValue(new Error("fail"));
71
+ await new Promise((resolve) => {
72
+ const req = http_1.default.request({ hostname: "localhost", port, path: "/ssrf", method: "GET" }, (res) => {
73
+ (0, globals_1.expect)(res.statusCode).toBe(500);
74
+ resolve();
75
+ });
76
+ req.end();
77
+ });
78
+ });
79
+ });
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SsrfExtractor = void 0;
4
+ class SsrfExtractor {
5
+ static extract(req) {
6
+ const headers = req.headers ?? {};
7
+ return {
8
+ ip: req.ip ?? headers["x-forwarded-for"] ?? "",
9
+ method: req.method,
10
+ path: req.path ?? req.url,
11
+ headers,
12
+ query: req.query ?? {},
13
+ raw: req,
14
+ };
15
+ }
16
+ }
17
+ exports.SsrfExtractor = SsrfExtractor;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SsrfListener = void 0;
7
+ const ssrf_extractor_1 = require("./ssrf.extractor");
8
+ const event_normalizer_1 = require("../../core/event.normalizer");
9
+ const logger_1 = require("../../core/logger");
10
+ const http_1 = __importDefault(require("http"));
11
+ class SsrfListener {
12
+ constructor(router, port) {
13
+ this.router = router;
14
+ this.logger = new logger_1.Logger({ context: "SsrfListener" });
15
+ this.server = http_1.default.createServer(async (req, res) => {
16
+ try {
17
+ const extracted = ssrf_extractor_1.SsrfExtractor.extract(req);
18
+ const event = event_normalizer_1.EventNormalizer.normalizeSsrf(extracted);
19
+ await this.router.dispatch(event);
20
+ res.writeHead(200);
21
+ res.end("OK");
22
+ }
23
+ catch (err) {
24
+ res.writeHead(500);
25
+ res.end("ERROR");
26
+ }
27
+ });
28
+ this.server.listen(port);
29
+ }
30
+ stop() {
31
+ this.server.close();
32
+ this.logger.info("SSRF Listener stopped");
33
+ }
34
+ }
35
+ exports.SsrfListener = SsrfListener;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TcpExtractor = void 0;
4
+ const id_generator_1 = require("../../core/id-generator");
5
+ class TcpExtractor {
6
+ static normalize(raw) {
7
+ return {
8
+ id: id_generator_1.IdGenerator.generate(),
9
+ type: "tcp",
10
+ timestamp: Date.now(),
11
+ ip: raw.ip ?? "",
12
+ port: raw.port ?? 0,
13
+ data: raw.data ?? "",
14
+ raw,
15
+ };
16
+ }
17
+ }
18
+ exports.TcpExtractor = TcpExtractor;