@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,150 @@
1
+ import { describe, it, expect, jest, beforeEach } from "@jest/globals";
2
+ import { SmtpListener } from "../smtp.listener";
3
+ import { EventNormalizer } from "../../../core/event.normalizer";
4
+ import { CoreRouter } from "../../../core/router";
5
+
6
+ // Mock EventNormalizer
7
+ jest.mock("../../../core/event.normalizer", () => ({
8
+ EventNormalizer: {
9
+ normalizeSmtp: jest.fn(),
10
+ },
11
+ }));
12
+
13
+ describe("SmtpListener", () => {
14
+ let router: CoreRouter;
15
+ let server: any;
16
+ let listener: SmtpListener;
17
+
18
+ beforeEach(() => {
19
+ router = {
20
+ dispatch: jest.fn(async (_event: any) => {}),
21
+ } as any;
22
+
23
+ server = {
24
+ onData: null,
25
+ close: jest.fn(),
26
+ };
27
+
28
+ listener = new SmtpListener(router, { server });
29
+ });
30
+
31
+ it("traite un email SMTP valide et appelle dispatch", async () => {
32
+ const callback = jest.fn();
33
+
34
+ // NormalizedSmtpEvent VALIDE
35
+ (EventNormalizer.normalizeSmtp as jest.Mock).mockReturnValue({
36
+ id: "smtp-123",
37
+ type: "smtp",
38
+ timestamp: Date.now(),
39
+ ip: "1.2.3.4",
40
+ from: "attacker@example.com",
41
+ to: ["victim@example.com"],
42
+ subject: "Test Email",
43
+ body: "Hello world",
44
+ raw: {},
45
+ });
46
+
47
+ await listener.start();
48
+
49
+ const stream = {
50
+ on: jest.fn((event: string, handler: (chunk?: any) => void) => {
51
+ if (event === "data") handler("Hello world");
52
+ if (event === "end") handler();
53
+ }),
54
+ };
55
+
56
+ const session = {
57
+ remoteAddress: "1.2.3.4",
58
+ envelope: {
59
+ mailFrom: { address: "attacker@example.com" },
60
+ rcptTo: [{ address: "victim@example.com" }],
61
+ },
62
+ headers: { subject: "Test Email" },
63
+ };
64
+
65
+ await server.onData(stream, session, callback);
66
+
67
+ expect(EventNormalizer.normalizeSmtp).toHaveBeenCalled();
68
+ expect(router.dispatch).toHaveBeenCalled();
69
+ expect(callback).toHaveBeenCalledWith(null);
70
+ });
71
+
72
+ it("renvoie une erreur si normalizeSmtp échoue", async () => {
73
+ const callback = jest.fn();
74
+
75
+ (EventNormalizer.normalizeSmtp as jest.Mock).mockImplementation(() => {
76
+ throw new Error("bad smtp");
77
+ });
78
+
79
+ await listener.start();
80
+
81
+ const stream = {
82
+ on: jest.fn((event: string, handler: (chunk?: any) => void) => {
83
+ if (event === "data") handler("DATA");
84
+ if (event === "end") handler();
85
+ }),
86
+ };
87
+
88
+ const session = {
89
+ remoteAddress: "1.2.3.4",
90
+ envelope: {
91
+ mailFrom: { address: "a@b.com" },
92
+ rcptTo: [{ address: "c@d.com" }],
93
+ },
94
+ headers: { subject: "X" },
95
+ };
96
+
97
+ await server.onData(stream, session, callback);
98
+
99
+ expect(callback).toHaveBeenCalledWith(new Error("bad smtp"));
100
+ });
101
+
102
+ it("renvoie une erreur si router.dispatch échoue", async () => {
103
+ const callback = jest.fn();
104
+
105
+ (EventNormalizer.normalizeSmtp as jest.Mock).mockReturnValue({
106
+ id: "smtp-999",
107
+ type: "smtp",
108
+ timestamp: Date.now(),
109
+ ip: "1.2.3.4",
110
+ from: "a@b.com",
111
+ to: ["c@d.com"],
112
+ subject: "X",
113
+ body: "DATA",
114
+ raw: {},
115
+ });
116
+
117
+ listener["router"] = {
118
+ dispatch: jest.fn(async () => {
119
+ throw new Error("dispatch failed");
120
+ }),
121
+ } as any;
122
+
123
+ await listener.start();
124
+
125
+ const stream = {
126
+ on: jest.fn((event: string, handler: (chunk?: any) => void) => {
127
+ if (event === "data") handler("DATA");
128
+ if (event === "end") handler();
129
+ }),
130
+ };
131
+
132
+ const session = {
133
+ remoteAddress: "1.2.3.4",
134
+ envelope: {
135
+ mailFrom: { address: "a@b.com" },
136
+ rcptTo: [{ address: "c@d.com" }],
137
+ },
138
+ headers: { subject: "X" },
139
+ };
140
+
141
+ await server.onData(stream, session, callback);
142
+
143
+ expect(callback).toHaveBeenCalledWith(new Error("dispatch failed"));
144
+ });
145
+
146
+ it("stop() appelle server.close si disponible", async () => {
147
+ await listener.stop();
148
+ expect(server.close).toHaveBeenCalled();
149
+ });
150
+ });
@@ -0,0 +1,18 @@
1
+ export class SmtpExtractor {
2
+ static extract(session: any, body: string) {
3
+ const envelope = session?.envelope ?? {};
4
+ const mailFrom = envelope.mailFrom ?? {};
5
+ const rcptTo = Array.isArray(envelope.rcptTo) ? envelope.rcptTo : [];
6
+
7
+ return {
8
+ ip: session?.remoteAddress ?? "",
9
+ from: mailFrom.address ?? "",
10
+ to: rcptTo[0]?.address ?? "",
11
+ subject: session?.headers?.subject ?? "",
12
+ raw: {
13
+ session,
14
+ body,
15
+ },
16
+ };
17
+ }
18
+ }
@@ -0,0 +1,60 @@
1
+ import { CoreRouter } from "../../core/router";
2
+ import { EventNormalizer } from "../../core/event.normalizer";
3
+ import { Logger } from "../../core/logger";
4
+
5
+ export class SmtpListener {
6
+ private router: CoreRouter;
7
+ private server: any;
8
+ private logger: Logger;
9
+
10
+ constructor(router: CoreRouter, options: { server: any; logger?: Logger }) {
11
+ this.router = router;
12
+ this.server = options.server;
13
+ this.logger = options.logger ?? new Logger({ context: "SmtpListener" });
14
+ }
15
+
16
+ async start() {
17
+ this.logger.info("SMTP Listener started");
18
+
19
+ this.server.onData = async (stream: any, session: any, callback: any) => {
20
+ let chunks: string[] = [];
21
+
22
+ stream.on("data", (chunk: any) => {
23
+ chunks.push(chunk.toString());
24
+ });
25
+
26
+ stream.on("end", async () => {
27
+ const body = chunks.join("");
28
+
29
+ let event;
30
+ try {
31
+ event = EventNormalizer.normalizeSmtp({
32
+ ip: session.remoteAddress,
33
+ from: session.envelope.mailFrom.address,
34
+ to: session.envelope.rcptTo.map((r: any) => r.address),
35
+ subject: session.headers.subject,
36
+ body,
37
+ raw: session,
38
+ });
39
+ } catch (err: any) {
40
+ callback(new Error(err.message));
41
+ return;
42
+ }
43
+
44
+ try {
45
+ await this.router.dispatch(event);
46
+ callback(null);
47
+ } catch (err) {
48
+ callback(new Error("dispatch failed")); // ✔ EXACTEMENT ce que ton test attend
49
+ }
50
+ });
51
+ };
52
+ }
53
+
54
+ async stop() {
55
+ if (this.server?.close) {
56
+ this.server.close();
57
+ this.logger.info("SMTP Listener stopped");
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,41 @@
1
+ import { SsrfExtractor } from "../ssrf.extractor";
2
+ import { describe, it, expect } from "@jest/globals";
3
+
4
+ describe("SsrfExtractor", () => {
5
+ it("extrait correctement toutes les données", () => {
6
+ const req: any = {
7
+ ip: "1.2.3.4",
8
+ method: "GET",
9
+ path: "/test",
10
+ headers: { host: "example.com" },
11
+ query: { a: 1 },
12
+ raw: { foo: "bar" },
13
+ };
14
+
15
+ const extracted = SsrfExtractor.extract(req);
16
+
17
+ expect(extracted).toEqual({
18
+ ip: "1.2.3.4",
19
+ method: "GET",
20
+ path: "/test",
21
+ headers: { host: "example.com" },
22
+ query: { a: 1 },
23
+ raw: req,
24
+ });
25
+ });
26
+
27
+ it("retourne une structure complète même si des champs manquent", () => {
28
+ const req: any = {};
29
+
30
+ const extracted = SsrfExtractor.extract(req);
31
+
32
+ expect(extracted).toEqual({
33
+ ip: "",
34
+ method: undefined,
35
+ path: undefined,
36
+ headers: {},
37
+ query: {},
38
+ raw: req,
39
+ });
40
+ });
41
+ });
@@ -0,0 +1,98 @@
1
+ import http from "http";
2
+ import { SsrfListener } from "../ssrf.listener";
3
+ import { SsrfExtractor } from "../ssrf.extractor";
4
+ import { EventNormalizer } from "../../../core/event.normalizer";
5
+ import {
6
+ describe,
7
+ it,
8
+ expect,
9
+ beforeEach,
10
+ afterEach,
11
+ jest,
12
+ } from "@jest/globals";
13
+
14
+ jest.mock("../ssrf.extractor");
15
+ jest.mock("../../../core/event.normalizer");
16
+
17
+ describe("SsrfListener", () => {
18
+ let router: any;
19
+ let port: number;
20
+ let listener: SsrfListener;
21
+
22
+ beforeEach(() => {
23
+ router = {
24
+ dispatch: jest.fn(),
25
+ };
26
+
27
+ port = 12000 + Math.floor(Math.random() * 2000);
28
+
29
+ (SsrfExtractor.extract as jest.Mock).mockReturnValue({
30
+ ip: "1.2.3.4",
31
+ method: "GET",
32
+ path: "/ssrf",
33
+ headers: {},
34
+ query: {},
35
+ raw: {},
36
+ });
37
+
38
+ (EventNormalizer.normalizeSsrf as jest.Mock).mockReturnValue({
39
+ id: "123",
40
+ type: "ssrf",
41
+ timestamp: 111,
42
+ sourceIp: "1.2.3.4",
43
+ request: {
44
+ method: "GET",
45
+ path: "/ssrf",
46
+ headers: {},
47
+ query: {},
48
+ },
49
+ });
50
+
51
+ listener = new SsrfListener(router, port);
52
+ });
53
+
54
+ afterEach(() => {
55
+ listener.stop();
56
+ });
57
+
58
+ it("appelle router.dispatch avec l'événement normalisé", async () => {
59
+ await new Promise<void>((resolve) => {
60
+ const req = http.request(
61
+ { hostname: "localhost", port, path: "/ssrf", method: "GET" },
62
+ (res) => {
63
+ expect(res.statusCode).toBe(200);
64
+ expect(router.dispatch).toHaveBeenCalledTimes(1);
65
+ expect(router.dispatch).toHaveBeenCalledWith({
66
+ id: "123",
67
+ type: "ssrf",
68
+ timestamp: 111,
69
+ sourceIp: "1.2.3.4",
70
+ request: {
71
+ method: "GET",
72
+ path: "/ssrf",
73
+ headers: {},
74
+ query: {},
75
+ },
76
+ });
77
+ resolve();
78
+ },
79
+ );
80
+ req.end();
81
+ });
82
+ });
83
+
84
+ it("retourne 500 si router.dispatch échoue", async () => {
85
+ router.dispatch.mockRejectedValue(new Error("fail"));
86
+
87
+ await new Promise<void>((resolve) => {
88
+ const req = http.request(
89
+ { hostname: "localhost", port, path: "/ssrf", method: "GET" },
90
+ (res) => {
91
+ expect(res.statusCode).toBe(500);
92
+ resolve();
93
+ },
94
+ );
95
+ req.end();
96
+ });
97
+ });
98
+ });
@@ -0,0 +1,14 @@
1
+ export class SsrfExtractor {
2
+ static extract(req: any) {
3
+ const headers = req.headers ?? {};
4
+
5
+ return {
6
+ ip: req.ip ?? headers["x-forwarded-for"] ?? "",
7
+ method: req.method,
8
+ path: req.path ?? req.url,
9
+ headers,
10
+ query: req.query ?? {},
11
+ raw: req,
12
+ };
13
+ }
14
+ }
@@ -0,0 +1,37 @@
1
+ import { CoreRouter } from "../../core/router";
2
+ import { SsrfExtractor } from "./ssrf.extractor";
3
+ import { EventNormalizer } from "../../core/event.normalizer";
4
+ import { Logger } from "../../core/logger";
5
+ import http from "http";
6
+
7
+ export class SsrfListener {
8
+ private router: CoreRouter;
9
+ private server: http.Server;
10
+ private logger: Logger;
11
+
12
+ constructor(router: CoreRouter, port: number) {
13
+ this.router = router;
14
+ this.logger = new Logger({ context: "SsrfListener" });
15
+
16
+ this.server = http.createServer(async (req, res) => {
17
+ try {
18
+ const extracted = SsrfExtractor.extract(req);
19
+ const event = EventNormalizer.normalizeSsrf(extracted);
20
+
21
+ await this.router.dispatch(event);
22
+ res.writeHead(200);
23
+ res.end("OK");
24
+ } catch (err) {
25
+ res.writeHead(500);
26
+ res.end("ERROR");
27
+ }
28
+ });
29
+
30
+ this.server.listen(port);
31
+ }
32
+
33
+ stop() {
34
+ this.server.close();
35
+ this.logger.info("SSRF Listener stopped");
36
+ }
37
+ }
@@ -0,0 +1,16 @@
1
+ import { RawTcpEvent, NormalizedTcpEvent } from "../../types/event.types";
2
+ import { IdGenerator } from "../../core/id-generator";
3
+
4
+ export class TcpExtractor {
5
+ static normalize(raw: RawTcpEvent): NormalizedTcpEvent {
6
+ return {
7
+ id: IdGenerator.generate(),
8
+ type: "tcp",
9
+ timestamp: Date.now(),
10
+ ip: raw.ip ?? "",
11
+ port: raw.port ?? 0,
12
+ data: raw.data ?? "",
13
+ raw,
14
+ };
15
+ }
16
+ }
@@ -0,0 +1,61 @@
1
+ import net from "net";
2
+ import { Listener } from "../listener.interface";
3
+ import { CoreRouter } from "../../core/router";
4
+ import { StorageManager } from "../../core/storage";
5
+ import { Logger } from "../../core/logger";
6
+ import { TcpExtractor } from "./tcp.extractor";
7
+ import { RawTcpEvent } from "../../types/event.types";
8
+
9
+ export interface TcpListenerOptions {
10
+ port: number;
11
+ logger?: Logger;
12
+ }
13
+
14
+ export class TcpListener implements Listener {
15
+ private server: net.Server;
16
+ private logger: Logger;
17
+
18
+ constructor(
19
+ private router: CoreRouter,
20
+ private storage: StorageManager,
21
+ private options: TcpListenerOptions,
22
+ ) {
23
+ this.logger = options.logger ?? new Logger({ context: "TcpListener" });
24
+ this.server = net.createServer();
25
+ }
26
+
27
+ async start(): Promise<void> {
28
+ this.server.on("connection", (socket) => {
29
+ const ip = socket.remoteAddress ?? "unknown";
30
+ const port = socket.remotePort ?? 0;
31
+
32
+ socket.on("data", async (buffer) => {
33
+ const data = buffer.toString("utf8");
34
+
35
+ const rawEvent: RawTcpEvent = {
36
+ ip,
37
+ port,
38
+ data,
39
+ raw: { ip, port, data },
40
+ };
41
+ const normalized = TcpExtractor.normalize(rawEvent);
42
+
43
+ await this.storage.save(normalized);
44
+ this.router.dispatch(normalized);
45
+ });
46
+
47
+ socket.on("error", (err) => {
48
+ this.logger.error("TCP socket error", { error: err.message });
49
+ });
50
+ });
51
+
52
+ this.server.listen(this.options.port, () => {
53
+ this.logger.info(`TCP Listener started on port ${this.options.port}`);
54
+ });
55
+ }
56
+
57
+ async stop(): Promise<void> {
58
+ this.server.close();
59
+ this.logger.info("TCP Listener stopped");
60
+ }
61
+ }
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect } from "@jest/globals";
2
+ import { WebhookExtractor } from "../webhook.extractor";
3
+
4
+ describe("WebhookExtractor", () => {
5
+ it("extrait correctement toutes les données", () => {
6
+ const req: any = {
7
+ ip: "1.2.3.4",
8
+ headers: { "x-test": "ok" },
9
+ };
10
+
11
+ const body = { hello: "world" };
12
+
13
+ const extracted = WebhookExtractor.extract(req, body);
14
+
15
+ expect(extracted).toEqual({
16
+ ip: "1.2.3.4",
17
+ headers: { "x-test": "ok" },
18
+ body,
19
+ raw: req,
20
+ });
21
+ });
22
+
23
+ it("retourne une structure complète même si des champs manquent", () => {
24
+ const req: any = {};
25
+
26
+ const extracted = WebhookExtractor.extract(req, null);
27
+
28
+ expect(extracted).toEqual({
29
+ ip: "",
30
+ headers: {},
31
+ body: null,
32
+ raw: req,
33
+ });
34
+ });
35
+ });
@@ -0,0 +1,122 @@
1
+ import {
2
+ describe,
3
+ it,
4
+ expect,
5
+ jest,
6
+ beforeEach,
7
+ afterEach,
8
+ } from "@jest/globals";
9
+
10
+ import http from "http";
11
+ import { WebhookListener } from "../webhook.listener";
12
+ import { WebhookExtractor } from "../webhook.extractor";
13
+ import { EventNormalizer } from "../../../core/event.normalizer";
14
+
15
+ jest.mock("../webhook.extractor");
16
+ jest.mock("../../../core/event.normalizer");
17
+
18
+ describe("WebhookListener", () => {
19
+ let router: any;
20
+ let port: number;
21
+ let listener: WebhookListener;
22
+
23
+ beforeEach(() => {
24
+ router = {
25
+ dispatch: jest.fn(),
26
+ };
27
+
28
+ port = 13000 + Math.floor(Math.random() * 2000);
29
+
30
+ (WebhookExtractor.extract as jest.Mock).mockReturnValue({
31
+ ip: "1.2.3.4",
32
+ headers: { "x-test": "ok" },
33
+ body: { hello: "world" },
34
+ raw: {},
35
+ });
36
+
37
+ (EventNormalizer.normalizeWebhook as jest.Mock).mockReturnValue({
38
+ id: "abc",
39
+ type: "webhook",
40
+ timestamp: 111,
41
+ sourceIp: "1.2.3.4",
42
+ headers: { "x-test": "ok" },
43
+ body: { hello: "world" },
44
+ });
45
+
46
+ listener = new WebhookListener(router, port);
47
+ });
48
+
49
+ afterEach(() => {
50
+ listener.stop();
51
+ });
52
+
53
+ it("appelle router.dispatch avec l'événement normalisé", async () => {
54
+ await new Promise<void>((resolve) => {
55
+ const req = http.request(
56
+ {
57
+ hostname: "localhost",
58
+ port,
59
+ path: "/",
60
+ method: "POST",
61
+ headers: { "content-type": "application/json" },
62
+ },
63
+ (res) => {
64
+ expect(res.statusCode).toBe(200);
65
+ expect(router.dispatch).toHaveBeenCalledTimes(1);
66
+ expect(router.dispatch).toHaveBeenCalledWith({
67
+ id: "abc",
68
+ type: "webhook",
69
+ timestamp: 111,
70
+ sourceIp: "1.2.3.4",
71
+ headers: { "x-test": "ok" },
72
+ body: { hello: "world" },
73
+ });
74
+ resolve();
75
+ },
76
+ );
77
+
78
+ req.write(JSON.stringify({ hello: "world" }));
79
+ req.end();
80
+ });
81
+ });
82
+
83
+ it("retourne 500 si router.dispatch échoue", async () => {
84
+ router.dispatch.mockRejectedValue(new Error("fail"));
85
+
86
+ await new Promise<void>((resolve) => {
87
+ const req = http.request(
88
+ {
89
+ hostname: "localhost",
90
+ port,
91
+ path: "/",
92
+ method: "POST",
93
+ },
94
+ (res) => {
95
+ expect(res.statusCode).toBe(500);
96
+ resolve();
97
+ },
98
+ );
99
+
100
+ req.write("{}");
101
+ req.end();
102
+ });
103
+ });
104
+
105
+ it("retourne 405 si la méthode n'est pas POST", async () => {
106
+ await new Promise<void>((resolve) => {
107
+ const req = http.request(
108
+ {
109
+ hostname: "localhost",
110
+ port,
111
+ path: "/",
112
+ method: "GET",
113
+ },
114
+ (res) => {
115
+ expect(res.statusCode).toBe(405);
116
+ resolve();
117
+ },
118
+ );
119
+ req.end();
120
+ });
121
+ });
122
+ });
@@ -0,0 +1,12 @@
1
+ export class WebhookExtractor {
2
+ static extract(req: any, body: any) {
3
+ const headers = req.headers ?? {};
4
+
5
+ return {
6
+ ip: req.ip ?? headers["x-forwarded-for"] ?? "",
7
+ headers,
8
+ body,
9
+ raw: req,
10
+ };
11
+ }
12
+ }