@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.
- package/.env.example +0 -0
- package/.github/workflows/ci.yml +29 -0
- package/.github/workflows/publish.yml +31 -0
- package/README.md +192 -0
- package/dist/api/controllers/__tests__/tasks.controller.test.js +61 -0
- package/dist/api/controllers/events.controller.js +13 -0
- package/dist/api/controllers/health.controller.js +11 -0
- package/dist/api/controllers/index.js +1 -0
- package/dist/api/controllers/tasks.controller.js +35 -0
- package/dist/api/dto/__tests__/create-task.dto.test.js +33 -0
- package/dist/api/dto/__tests__/filter-tasks.dto.test.js +28 -0
- package/dist/api/dto/create-task.dto.js +26 -0
- package/dist/api/dto/filter-tasks.dto.js +27 -0
- package/dist/api/services/__tests__/events.service.test.js +25 -0
- package/dist/api/services/__tests__/tasks.service.test.js +25 -0
- package/dist/api/services/events.service.js +18 -0
- package/dist/api/services/tasks.service.js +52 -0
- package/dist/api/sse/events.stream.js +63 -0
- package/dist/config/constants.js +1 -0
- package/dist/config/env.js +1 -0
- package/dist/core/__tests__/core-router.test.js +26 -0
- package/dist/core/__tests__/core-server.test.js +39 -0
- package/dist/core/__tests__/event.normalizer.test.js +50 -0
- package/dist/core/__tests__/event.router.test.js +66 -0
- package/dist/core/__tests__/logger.test.js +26 -0
- package/dist/core/__tests__/storage-manager.test.js +57 -0
- package/dist/core/event.normalizer.js +126 -0
- package/dist/core/event.router.js +15 -0
- package/dist/core/http/__tests__/adapter-node.test.js +74 -0
- package/dist/core/http/__tests__/body-parser-multipart.test.js +35 -0
- package/dist/core/http/__tests__/body-parser-raw.test.js +25 -0
- package/dist/core/http/__tests__/body-parser-text.test.js +25 -0
- package/dist/core/http/__tests__/compile-path.test.js +33 -0
- package/dist/core/http/__tests__/middleware-pipeline.test.js +39 -0
- package/dist/core/http/__tests__/request.test.js +32 -0
- package/dist/core/http/__tests__/response.test.js +26 -0
- package/dist/core/http/__tests__/router-match.test.js +117 -0
- package/dist/core/http/adapter-node.js +44 -0
- package/dist/core/http/buildRequest.js +16 -0
- package/dist/core/http/compile-path.js +30 -0
- package/dist/core/http/errors.js +35 -0
- package/dist/core/http/http-server.js +48 -0
- package/dist/core/http/index.js +1 -0
- package/dist/core/http/main.js +1 -0
- package/dist/core/http/middleware.js +133 -0
- package/dist/core/http/request.js +22 -0
- package/dist/core/http/response.js +74 -0
- package/dist/core/http/router.js +111 -0
- package/dist/core/http/utils.js +1 -0
- package/dist/core/id-generator.js +14 -0
- package/dist/core/logger.js +81 -0
- package/dist/core/router.js +30 -0
- package/dist/core/server.js +70 -0
- package/dist/core/storage.js +46 -0
- package/dist/index.js +76 -0
- package/dist/listeners/api/__tests__/api.controller.test.js +88 -0
- package/dist/listeners/api/__tests__/api.extractor.test.js +39 -0
- package/dist/listeners/api/__tests__/api.listener.test.js +66 -0
- package/dist/listeners/api/__tests__/api.routes.test.js +105 -0
- package/dist/listeners/api/__tests__/api.sse.test.js +78 -0
- package/dist/listeners/api/api.controllers.js +39 -0
- package/dist/listeners/api/api.extractor.js +41 -0
- package/dist/listeners/api/api.listener.js +37 -0
- package/dist/listeners/api/api.routes.js +59 -0
- package/dist/listeners/api/api.sse.js +35 -0
- package/dist/listeners/dns/__tests__/dns.test.js +89 -0
- package/dist/listeners/dns/dns.extractor.js +17 -0
- package/dist/listeners/dns/dns.listener.js +48 -0
- package/dist/listeners/http/__tests__/http.extractor.test.js +52 -0
- package/dist/listeners/http/__tests__/http.listener.test.js +106 -0
- package/dist/listeners/http/http.extractor.js +18 -0
- package/dist/listeners/http/http.listener.js +91 -0
- package/dist/listeners/listener.interface.js +2 -0
- package/dist/listeners/smtp/__tests__/smtp.extractor.test.js +62 -0
- package/dist/listeners/smtp/__tests__/smtp.listener.test.js +129 -0
- package/dist/listeners/smtp/smtp.extractor.js +21 -0
- package/dist/listeners/smtp/smtp.listener.js +53 -0
- package/dist/listeners/ssrf/__tests__/ssrf.extractor.test.js +37 -0
- package/dist/listeners/ssrf/__tests__/ssrf.listener.test.js +79 -0
- package/dist/listeners/ssrf/ssrf.extractor.js +17 -0
- package/dist/listeners/ssrf/ssrf.listener.js +35 -0
- package/dist/listeners/tcp/tcp.extractor.js +18 -0
- package/dist/listeners/tcp/tcp.listener.js +47 -0
- package/dist/listeners/webhook/__tests__/webhook.extractor.test.js +30 -0
- package/dist/listeners/webhook/__tests__/webhook.listener.test.js +96 -0
- package/dist/listeners/webhook/webhook.extractor.js +15 -0
- package/dist/listeners/webhook/webhook.listener.js +51 -0
- package/dist/listeners/websocket/__tests__/websocket.extractor.test.js +29 -0
- package/dist/listeners/websocket/__tests__/websocket.listener.test.js +73 -0
- package/dist/listeners/websocket/websocket.extractor.js +14 -0
- package/dist/listeners/websocket/websocket.listener.js +33 -0
- package/dist/storage-adapters/adapters/__tests__/memory.storage.test.js +64 -0
- package/dist/storage-adapters/adapters/memory.storage.js +48 -0
- package/dist/storage-adapters/adapters/redis.storage.js +1 -0
- package/dist/storage-adapters/adapters/sqlite.storage.js +1 -0
- package/dist/storage-adapters/storage.interface.js +2 -0
- package/dist/types/event.types.js +2 -0
- package/dist/utils/token.js +1 -0
- package/image.png +0 -0
- package/jest.config.js +11 -0
- package/package.json +45 -0
- package/sadmin list shadows +9 -0
- package/src/api/controllers/__tests__/tasks.controller.test.ts +74 -0
- package/src/api/controllers/events.controller.ts +10 -0
- package/src/api/controllers/health.controller.ts +7 -0
- package/src/api/controllers/index.ts +0 -0
- package/src/api/controllers/tasks.controller.ts +41 -0
- package/src/api/dto/__tests__/create-task.dto.test.ts +41 -0
- package/src/api/dto/__tests__/filter-tasks.dto.test.ts +35 -0
- package/src/api/dto/create-task.dto.ts +33 -0
- package/src/api/dto/filter-tasks.dto.ts +33 -0
- package/src/api/services/__tests__/events.service.test.ts +41 -0
- package/src/api/services/__tests__/tasks.service.test.ts +41 -0
- package/src/api/services/events.service.ts +17 -0
- package/src/api/services/tasks.service.ts +79 -0
- package/src/api/sse/events.stream.ts +90 -0
- package/src/config/constants.ts +0 -0
- package/src/config/env.ts +0 -0
- package/src/core/__tests__/core-router.test.ts +30 -0
- package/src/core/__tests__/core-server.test.ts +44 -0
- package/src/core/__tests__/event.normalizer.test.ts +56 -0
- package/src/core/__tests__/event.router.test.ts +89 -0
- package/src/core/__tests__/logger.test.ts +32 -0
- package/src/core/__tests__/storage-manager.test.ts +74 -0
- package/src/core/event.normalizer.ts +147 -0
- package/src/core/event.router.ts +13 -0
- package/src/core/http/__tests__/adapter-node.test.ts +52 -0
- package/src/core/http/__tests__/body-parser-multipart.test.ts +41 -0
- package/src/core/http/__tests__/body-parser-raw.test.ts +28 -0
- package/src/core/http/__tests__/body-parser-text.test.ts +28 -0
- package/src/core/http/__tests__/compile-path.test.ts +39 -0
- package/src/core/http/__tests__/middleware-pipeline.test.ts +51 -0
- package/src/core/http/__tests__/request.test.ts +34 -0
- package/src/core/http/__tests__/response.test.ts +35 -0
- package/src/core/http/__tests__/router-match.test.ts +171 -0
- package/src/core/http/adapter-node.ts +51 -0
- package/src/core/http/buildRequest.ts +18 -0
- package/src/core/http/compile-path.ts +32 -0
- package/src/core/http/errors.ts +37 -0
- package/src/core/http/http-server.ts +52 -0
- package/src/core/http/index.ts +0 -0
- package/src/core/http/main.ts +0 -0
- package/src/core/http/middleware.ts +160 -0
- package/src/core/http/request.ts +55 -0
- package/src/core/http/response.ts +93 -0
- package/src/core/http/router.ts +138 -0
- package/src/core/http/utils.ts +0 -0
- package/src/core/id-generator.ts +8 -0
- package/src/core/logger.ts +113 -0
- package/src/core/router.ts +44 -0
- package/src/core/server.ts +85 -0
- package/src/core/storage.ts +64 -0
- package/src/index.ts +89 -0
- package/src/listeners/api/__tests__/api.controller.test.ts +116 -0
- package/src/listeners/api/__tests__/api.extractor.test.ts +46 -0
- package/src/listeners/api/__tests__/api.listener.test.ts +82 -0
- package/src/listeners/api/__tests__/api.routes.test.ts +155 -0
- package/src/listeners/api/__tests__/api.sse.test.ts +105 -0
- package/src/listeners/api/api.controllers.ts +67 -0
- package/src/listeners/api/api.extractor.ts +43 -0
- package/src/listeners/api/api.listener.ts +50 -0
- package/src/listeners/api/api.routes.ts +76 -0
- package/src/listeners/api/api.sse.ts +38 -0
- package/src/listeners/dns/__tests__/dns.test.ts +118 -0
- package/src/listeners/dns/dns.extractor.ts +14 -0
- package/src/listeners/dns/dns.listener.ts +61 -0
- package/src/listeners/http/__tests__/http.extractor.test.ts +59 -0
- package/src/listeners/http/__tests__/http.listener.test.ts +133 -0
- package/src/listeners/http/http.extractor.ts +15 -0
- package/src/listeners/http/http.listener.ts +110 -0
- package/src/listeners/listener.interface.ts +4 -0
- package/src/listeners/smtp/__tests__/smtp.extractor.test.ts +69 -0
- package/src/listeners/smtp/__tests__/smtp.listener.test.ts +150 -0
- package/src/listeners/smtp/smtp.extractor.ts +18 -0
- package/src/listeners/smtp/smtp.listener.ts +60 -0
- package/src/listeners/ssrf/__tests__/ssrf.extractor.test.ts +41 -0
- package/src/listeners/ssrf/__tests__/ssrf.listener.test.ts +98 -0
- package/src/listeners/ssrf/ssrf.extractor.ts +14 -0
- package/src/listeners/ssrf/ssrf.listener.ts +37 -0
- package/src/listeners/tcp/tcp.extractor.ts +16 -0
- package/src/listeners/tcp/tcp.listener.ts +61 -0
- package/src/listeners/webhook/__tests__/webhook.extractor.test.ts +35 -0
- package/src/listeners/webhook/__tests__/webhook.listener.test.ts +122 -0
- package/src/listeners/webhook/webhook.extractor.ts +12 -0
- package/src/listeners/webhook/webhook.listener.ts +58 -0
- package/src/listeners/websocket/__tests__/websocket.extractor.test.ts +33 -0
- package/src/listeners/websocket/__tests__/websocket.listener.test.ts +90 -0
- package/src/listeners/websocket/websocket.extractor.ts +11 -0
- package/src/listeners/websocket/websocket.listener.ts +40 -0
- package/src/storage-adapters/adapters/__tests__/memory.storage.test.ts +75 -0
- package/src/storage-adapters/adapters/memory.storage.ts +64 -0
- package/src/storage-adapters/adapters/redis.storage.ts +0 -0
- package/src/storage-adapters/adapters/sqlite.storage.ts +0 -0
- package/src/storage-adapters/storage.interface.ts +26 -0
- package/src/types/event.types.ts +147 -0
- package/src/utils/token.ts +0 -0
- package/src-api.txt +0 -0
- package/src-architecture.txt +0 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// src/http/router.ts
|
|
2
|
+
|
|
3
|
+
import { compilePath } from "./compile-path";
|
|
4
|
+
import { Middleware } from "./middleware";
|
|
5
|
+
|
|
6
|
+
export class Router {
|
|
7
|
+
private routes: Array<{
|
|
8
|
+
method: string;
|
|
9
|
+
path: string;
|
|
10
|
+
compiled: any[];
|
|
11
|
+
middlewares: Middleware[];
|
|
12
|
+
handler: Function;
|
|
13
|
+
}> = [];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Enregistre une route avec middlewares optionnels :
|
|
17
|
+
* router.get("/x", mw1, mw2, handler)
|
|
18
|
+
*/
|
|
19
|
+
register(method: string, path: string, ...middlewaresAndHandler: any[]) {
|
|
20
|
+
const compiled = compilePath(path);
|
|
21
|
+
|
|
22
|
+
const handler = middlewaresAndHandler.pop();
|
|
23
|
+
const middlewares = middlewaresAndHandler as Middleware[];
|
|
24
|
+
|
|
25
|
+
this.routes.push({
|
|
26
|
+
method,
|
|
27
|
+
path,
|
|
28
|
+
compiled,
|
|
29
|
+
middlewares,
|
|
30
|
+
handler,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Trouve la route et extrait les params
|
|
36
|
+
*/
|
|
37
|
+
match(method: string, urlPath: string) {
|
|
38
|
+
const urlSegments = urlPath.replace(/^\//, "").split("/");
|
|
39
|
+
|
|
40
|
+
for (const route of this.routes) {
|
|
41
|
+
if (route.method !== method) continue;
|
|
42
|
+
|
|
43
|
+
const params: Record<string, string> = {};
|
|
44
|
+
const compiled = route.compiled;
|
|
45
|
+
|
|
46
|
+
if (this.matchSegments(compiled, urlSegments, params)) {
|
|
47
|
+
return {
|
|
48
|
+
handler: route.handler,
|
|
49
|
+
middlewares: route.middlewares,
|
|
50
|
+
params,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Ton moteur de matching existant — inchangé
|
|
60
|
+
*/
|
|
61
|
+
private matchSegments(
|
|
62
|
+
compiled: any[],
|
|
63
|
+
urlSegments: string[],
|
|
64
|
+
params: Record<string, string>,
|
|
65
|
+
) {
|
|
66
|
+
let i = 0;
|
|
67
|
+
|
|
68
|
+
for (let j = 0; j < compiled.length; j++) {
|
|
69
|
+
const segment = compiled[j];
|
|
70
|
+
const part = urlSegments[i];
|
|
71
|
+
|
|
72
|
+
if (!part && segment.type !== "wildcard") {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (segment.type === "static") {
|
|
77
|
+
if (segment.value !== part) return false;
|
|
78
|
+
i++;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (segment.type === "param") {
|
|
83
|
+
if (!part) return false;
|
|
84
|
+
params[segment.name] = part;
|
|
85
|
+
i++;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (segment.type === "wildcard") {
|
|
90
|
+
if (!part) return false;
|
|
91
|
+
|
|
92
|
+
const next = compiled[j + 1];
|
|
93
|
+
if (!next) return true;
|
|
94
|
+
|
|
95
|
+
while (i < urlSegments.length) {
|
|
96
|
+
if (next.type === "static" && urlSegments[i] === next.value) {
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
i++;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (i >= urlSegments.length) return false;
|
|
103
|
+
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return i === urlSegments.length;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get(path: string, ...middlewaresAndHandler: any[]) {
|
|
112
|
+
this.register("GET", path, ...middlewaresAndHandler);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
post(path: string, ...middlewaresAndHandler: any[]) {
|
|
116
|
+
this.register("POST", path, ...middlewaresAndHandler);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
put(path: string, ...middlewaresAndHandler: any[]) {
|
|
120
|
+
this.register("PUT", path, ...middlewaresAndHandler);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
delete(path: string, ...middlewaresAndHandler: any[]) {
|
|
124
|
+
this.register("DELETE", path, ...middlewaresAndHandler);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
patch(path: string, ...middlewaresAndHandler: any[]) {
|
|
128
|
+
this.register("PATCH", path, ...middlewaresAndHandler);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
options(path: string, ...middlewaresAndHandler: any[]) {
|
|
132
|
+
this.register("OPTIONS", path, ...middlewaresAndHandler);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
head(path: string, ...middlewaresAndHandler: any[]) {
|
|
136
|
+
this.register("HEAD", path, ...middlewaresAndHandler);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export type LogLevel = "debug" | "info" | "warn" | "error" | "fatal";
|
|
2
|
+
|
|
3
|
+
export interface LoggerOptions {
|
|
4
|
+
level?: LogLevel;
|
|
5
|
+
context?: string;
|
|
6
|
+
enabled?: boolean;
|
|
7
|
+
hooks?: Array<(entry: LogEntry) => void>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface LogEntry {
|
|
11
|
+
timestamp: string;
|
|
12
|
+
level: LogLevel;
|
|
13
|
+
message: string;
|
|
14
|
+
context?: string;
|
|
15
|
+
data?: any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const COLORS = {
|
|
19
|
+
reset: "\x1b[0m",
|
|
20
|
+
gray: "\x1b[90m",
|
|
21
|
+
blue: "\x1b[34m",
|
|
22
|
+
yellow: "\x1b[33m",
|
|
23
|
+
red: "\x1b[31m",
|
|
24
|
+
magenta: "\x1b[35m",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const LEVEL_COLORS: Record<LogLevel, string> = {
|
|
28
|
+
debug: COLORS.gray,
|
|
29
|
+
info: COLORS.blue,
|
|
30
|
+
warn: COLORS.yellow,
|
|
31
|
+
error: COLORS.red,
|
|
32
|
+
fatal: COLORS.magenta,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const LEVEL_ORDER: Record<LogLevel, number> = {
|
|
36
|
+
debug: 10,
|
|
37
|
+
info: 20,
|
|
38
|
+
warn: 30,
|
|
39
|
+
error: 40,
|
|
40
|
+
fatal: 50,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export class Logger {
|
|
44
|
+
private level: LogLevel;
|
|
45
|
+
private context?: string;
|
|
46
|
+
private enabled: boolean;
|
|
47
|
+
private hooks: Array<(entry: LogEntry) => void>;
|
|
48
|
+
|
|
49
|
+
constructor(options: LoggerOptions = {}) {
|
|
50
|
+
this.level = options.level ?? "info";
|
|
51
|
+
this.context = options.context;
|
|
52
|
+
this.enabled = options.enabled ?? true;
|
|
53
|
+
this.hooks = options.hooks ?? [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private shouldLog(level: LogLevel): boolean {
|
|
57
|
+
return this.enabled && LEVEL_ORDER[level] >= LEVEL_ORDER[this.level];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private emit(level: LogLevel, message: string, data?: any) {
|
|
61
|
+
if (!this.shouldLog(level)) return;
|
|
62
|
+
|
|
63
|
+
const entry: LogEntry = {
|
|
64
|
+
timestamp: new Date().toISOString(),
|
|
65
|
+
level,
|
|
66
|
+
message,
|
|
67
|
+
context: this.context,
|
|
68
|
+
data,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Console output
|
|
72
|
+
const color = LEVEL_COLORS[level];
|
|
73
|
+
const ctx = this.context ? `[${this.context}] ` : "";
|
|
74
|
+
const formatted = `${color}${entry.timestamp} ${level.toUpperCase()} ${ctx}${message}${COLORS.reset}`;
|
|
75
|
+
|
|
76
|
+
console.log(formatted);
|
|
77
|
+
|
|
78
|
+
// Hooks (dashboard, file, webhook…)
|
|
79
|
+
for (const hook of this.hooks) {
|
|
80
|
+
hook(entry);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
debug(msg: string, data?: any) {
|
|
85
|
+
this.emit("debug", msg, data);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
info(msg: string, data?: any) {
|
|
89
|
+
this.emit("info", msg, data);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
warn(msg: string, data?: any) {
|
|
93
|
+
this.emit("warn", msg, data);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
error(msg: string, data?: any) {
|
|
97
|
+
this.emit("error", msg, data);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fatal(msg: string, data?: any) {
|
|
101
|
+
this.emit("fatal", msg, data);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Créer un logger enfant avec un contexte différent
|
|
105
|
+
withContext(context: string): Logger {
|
|
106
|
+
return new Logger({
|
|
107
|
+
level: this.level,
|
|
108
|
+
enabled: this.enabled,
|
|
109
|
+
hooks: this.hooks,
|
|
110
|
+
context,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Storage } from "../storage-adapters/storage.interface";
|
|
2
|
+
import { Logger } from "./logger";
|
|
3
|
+
import { AnyNormalizedEvent } from "../types/event.types";
|
|
4
|
+
|
|
5
|
+
export type RouterHook = (event: AnyNormalizedEvent) => void | Promise<void>;
|
|
6
|
+
|
|
7
|
+
export interface RouterOptions {
|
|
8
|
+
logger?: Logger;
|
|
9
|
+
hooks?: RouterHook[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class CoreRouter {
|
|
13
|
+
private logger: Logger;
|
|
14
|
+
private hooks: RouterHook[];
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private storage: Storage,
|
|
18
|
+
options: RouterOptions = {},
|
|
19
|
+
) {
|
|
20
|
+
this.logger = options.logger ?? new Logger({ context: "CoreRouter" });
|
|
21
|
+
this.hooks = options.hooks ?? [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async dispatch(event: AnyNormalizedEvent): Promise<void> {
|
|
25
|
+
// 1. Log
|
|
26
|
+
this.logger.info(`Event received (${event.type})`, { id: event.id });
|
|
27
|
+
|
|
28
|
+
// 2. Save
|
|
29
|
+
await this.storage.save(event);
|
|
30
|
+
|
|
31
|
+
// 3. Hooks
|
|
32
|
+
for (const hook of this.hooks) {
|
|
33
|
+
try {
|
|
34
|
+
await hook(event);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
this.logger.error("Hook error", err);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
addHook(hook: RouterHook) {
|
|
42
|
+
this.hooks.push(hook);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Logger } from "./logger";
|
|
2
|
+
import { CoreRouter } from "./router";
|
|
3
|
+
import { StorageManager } from "./storage";
|
|
4
|
+
import { Listener } from "../listeners/listener.interface";
|
|
5
|
+
export interface CoreServerOptions {
|
|
6
|
+
logger?: Logger;
|
|
7
|
+
storage?: StorageManager;
|
|
8
|
+
listeners?: Listener[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class CoreServer {
|
|
12
|
+
private logger: Logger;
|
|
13
|
+
private storage: StorageManager;
|
|
14
|
+
private router: CoreRouter;
|
|
15
|
+
private listeners: Listener[];
|
|
16
|
+
|
|
17
|
+
constructor(options: CoreServerOptions = {}) {
|
|
18
|
+
this.logger = options.logger ?? new Logger({ context: "CoreServer" });
|
|
19
|
+
this.storage =
|
|
20
|
+
options.storage ?? new StorageManager({ logger: this.logger });
|
|
21
|
+
this.router = new CoreRouter(this.storage, { logger: this.logger });
|
|
22
|
+
this.listeners = options.listeners ?? [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getRouter() {
|
|
26
|
+
return this.router;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getStorage() {
|
|
30
|
+
return this.storage;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async start(): Promise<void> {
|
|
34
|
+
this.logger.info("Starting OAST server...");
|
|
35
|
+
|
|
36
|
+
// Démarrage des listeners
|
|
37
|
+
for (const listener of this.listeners) {
|
|
38
|
+
try {
|
|
39
|
+
await listener.start();
|
|
40
|
+
this.logger.info(`Listener started`, {
|
|
41
|
+
listener: listener.constructor.name,
|
|
42
|
+
});
|
|
43
|
+
} catch (err: any) {
|
|
44
|
+
this.logger.error("Listener failed to start", {
|
|
45
|
+
listener: listener.constructor.name,
|
|
46
|
+
error: err.message,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.logger.info("OAST server started");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async stop(): Promise<void> {
|
|
55
|
+
this.logger.info("Stopping OAST server...");
|
|
56
|
+
|
|
57
|
+
for (const listener of this.listeners) {
|
|
58
|
+
try {
|
|
59
|
+
await listener.stop();
|
|
60
|
+
this.logger.info(`Listener stopped`, {
|
|
61
|
+
listener: listener.constructor.name,
|
|
62
|
+
});
|
|
63
|
+
} catch (err: any) {
|
|
64
|
+
this.logger.error("Listener failed to stop", {
|
|
65
|
+
listener: listener.constructor.name,
|
|
66
|
+
error: err.message,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.logger.info("OAST server stopped");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Méthode appelée par CoreRouter après chaque event normalisé.
|
|
76
|
+
* Permet de diffuser l’event aux listeners SSE (ApiListener).
|
|
77
|
+
*/
|
|
78
|
+
broadcast(event: any) {
|
|
79
|
+
for (const listener of this.listeners) {
|
|
80
|
+
if (typeof (listener as any).broadcastEvent === "function") {
|
|
81
|
+
(listener as any).broadcastEvent(event);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Storage } from "../storage-adapters/storage.interface";
|
|
2
|
+
import { AnyNormalizedEvent } from "../types/event.types";
|
|
3
|
+
import { Logger } from "./logger";
|
|
4
|
+
|
|
5
|
+
export class StorageManager implements Storage {
|
|
6
|
+
private events: AnyNormalizedEvent[] = [];
|
|
7
|
+
private logger: Logger;
|
|
8
|
+
|
|
9
|
+
constructor(options: { logger?: Logger } = {}) {
|
|
10
|
+
this.logger = options.logger ?? new Logger({ context: "StorageManager" });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async save(event: AnyNormalizedEvent): Promise<void> {
|
|
14
|
+
this.events.push(event);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async getEvent(id: string): Promise<AnyNormalizedEvent | null> {
|
|
18
|
+
return this.events.find((e) => e.id === id) ?? null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async listEvents(params: {
|
|
22
|
+
type?: string;
|
|
23
|
+
page?: number;
|
|
24
|
+
limit?: number;
|
|
25
|
+
}): Promise<AnyNormalizedEvent[]> {
|
|
26
|
+
const { type, page = 1, limit = 50 } = params;
|
|
27
|
+
|
|
28
|
+
let filtered = this.events;
|
|
29
|
+
if (type) filtered = filtered.filter((e) => e.type === type);
|
|
30
|
+
|
|
31
|
+
const start = (page - 1) * limit;
|
|
32
|
+
return filtered.slice(start, start + limit);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async getAll(): Promise<AnyNormalizedEvent[]> {
|
|
36
|
+
return this.events;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async deleteEvent(id: string): Promise<boolean> {
|
|
40
|
+
const before = this.events.length;
|
|
41
|
+
this.events = this.events.filter((e) => e.id !== id);
|
|
42
|
+
return this.events.length !== before;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async clearEvents(): Promise<void> {
|
|
46
|
+
this.events = [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async getStats(): Promise<{
|
|
50
|
+
total: number;
|
|
51
|
+
byType: Record<string, number>;
|
|
52
|
+
}> {
|
|
53
|
+
const byType: Record<string, number> = {};
|
|
54
|
+
|
|
55
|
+
for (const e of this.events) {
|
|
56
|
+
byType[e.type] = (byType[e.type] ?? 0) + 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
total: this.events.length,
|
|
61
|
+
byType,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Logger } from "./core/logger";
|
|
2
|
+
import { StorageManager } from "./core/storage";
|
|
3
|
+
import { CoreRouter } from "./core/router";
|
|
4
|
+
import { CoreServer } from "./core/server";
|
|
5
|
+
|
|
6
|
+
// Listeners
|
|
7
|
+
import { ApiListener } from "./listeners/api/api.listener";
|
|
8
|
+
import { HttpListener } from "./listeners/http/http.listener";
|
|
9
|
+
import { DnsListener } from "./listeners/dns/dns.listener";
|
|
10
|
+
import { SmtpListener } from "./listeners/smtp/smtp.listener";
|
|
11
|
+
import { TcpListener } from "./listeners/tcp/tcp.listener";
|
|
12
|
+
|
|
13
|
+
async function main() {
|
|
14
|
+
const logger = new Logger({ context: "Bootstrap" });
|
|
15
|
+
|
|
16
|
+
// Core components
|
|
17
|
+
const storage = new StorageManager({
|
|
18
|
+
logger: logger.withContext("Storage"),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const router = new CoreRouter(storage, {
|
|
22
|
+
logger: logger.withContext("CoreRouter"),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// ---------------------------
|
|
26
|
+
// LISTENERS
|
|
27
|
+
// ---------------------------
|
|
28
|
+
|
|
29
|
+
// API → (router, storage, options)
|
|
30
|
+
const apiListener = new ApiListener(storage, {
|
|
31
|
+
port: 3100,
|
|
32
|
+
logger: logger.withContext("ApiListener"),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// HTTP → (router, options)
|
|
36
|
+
const httpListener = new HttpListener(router, {
|
|
37
|
+
port: 8080,
|
|
38
|
+
logger: logger.withContext("HttpListener"),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// DNS → (router, options)
|
|
42
|
+
const dnsListener = new DnsListener(router, {
|
|
43
|
+
server: {} as any,
|
|
44
|
+
logger: logger.withContext("DnsListener"),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const smtpListener = new SmtpListener(router, {
|
|
48
|
+
server: {} as any,
|
|
49
|
+
logger: logger.withContext("SmtpListener"),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// TCP → (router, storage, options)
|
|
53
|
+
const tcpListener = new TcpListener(router, storage, {
|
|
54
|
+
port: 9000,
|
|
55
|
+
logger: logger.withContext("TcpListener"),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ---------------------------
|
|
59
|
+
// CORE SERVER
|
|
60
|
+
// ---------------------------
|
|
61
|
+
const server = new CoreServer({
|
|
62
|
+
logger: logger.withContext("CoreServer"),
|
|
63
|
+
storage,
|
|
64
|
+
listeners: [
|
|
65
|
+
apiListener,
|
|
66
|
+
httpListener,
|
|
67
|
+
dnsListener,
|
|
68
|
+
smtpListener,
|
|
69
|
+
tcpListener,
|
|
70
|
+
],
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await server.start();
|
|
74
|
+
|
|
75
|
+
// Graceful shutdown
|
|
76
|
+
const shutdown = async () => {
|
|
77
|
+
logger.warn("Received shutdown signal, stopping server...");
|
|
78
|
+
await server.stop();
|
|
79
|
+
process.exit(0);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
process.on("SIGINT", shutdown);
|
|
83
|
+
process.on("SIGTERM", shutdown);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
main().catch((err) => {
|
|
87
|
+
console.error("Fatal error in bootstrap:", err);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { ApiController } from "../api.controllers";
|
|
2
|
+
import { StorageManager } from "../../../core/storage";
|
|
3
|
+
import { ServerResponse, IncomingMessage } from "http";
|
|
4
|
+
import { Socket } from "net";
|
|
5
|
+
import { describe, it, expect, beforeEach, jest } from "@jest/globals";
|
|
6
|
+
|
|
7
|
+
// Mock ServerResponse
|
|
8
|
+
function mockRes(): ServerResponse {
|
|
9
|
+
const socket = new Socket();
|
|
10
|
+
const req = new IncomingMessage(socket);
|
|
11
|
+
const res = new ServerResponse(req);
|
|
12
|
+
|
|
13
|
+
jest.spyOn(res, "writeHead");
|
|
14
|
+
jest.spyOn(res, "end");
|
|
15
|
+
|
|
16
|
+
return res;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("ApiController", () => {
|
|
20
|
+
let storage: StorageManager;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
// On utilise un vrai StorageManager en mémoire
|
|
24
|
+
storage = new StorageManager();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("listEvents renvoie la liste des events (vide par défaut)", async () => {
|
|
28
|
+
const res = mockRes();
|
|
29
|
+
const url = new URL("http://localhost/events?page=2&type=http");
|
|
30
|
+
|
|
31
|
+
await ApiController.listEvents(url, res, storage);
|
|
32
|
+
|
|
33
|
+
expect(storage.listEvents).toBeDefined();
|
|
34
|
+
|
|
35
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, {
|
|
36
|
+
"Content-Type": "application/json",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const body = (res.end as jest.Mock).mock.calls[0][0] as string;
|
|
40
|
+
const payload = JSON.parse(body);
|
|
41
|
+
expect(payload.success).toBe(true);
|
|
42
|
+
expect(payload.events).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("getEvent renvoie 404 si event absent", async () => {
|
|
46
|
+
const res = mockRes();
|
|
47
|
+
|
|
48
|
+
await ApiController.getEvent("unknown", res, storage);
|
|
49
|
+
|
|
50
|
+
expect(res.writeHead).toHaveBeenCalledWith(404, {
|
|
51
|
+
"Content-Type": "application/json",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const body = (res.end as jest.Mock).mock.calls[0][0] as string;
|
|
55
|
+
const payload = JSON.parse(body);
|
|
56
|
+
expect(payload.success).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("getEvent renvoie l'event si présent", async () => {
|
|
60
|
+
// On mocke juste getEvent pour ce test
|
|
61
|
+
jest.spyOn(storage, "getEvent").mockResolvedValue({ id: "abc" } as any);
|
|
62
|
+
|
|
63
|
+
const res = mockRes();
|
|
64
|
+
|
|
65
|
+
await ApiController.getEvent("abc", res, storage);
|
|
66
|
+
|
|
67
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, {
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const body = (res.end as jest.Mock).mock.calls[0][0] as string;
|
|
72
|
+
const payload = JSON.parse(body);
|
|
73
|
+
expect(payload.success).toBe(true);
|
|
74
|
+
expect(payload.event.id).toBe("abc");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("deleteAll supprime tous les events", async () => {
|
|
78
|
+
const res = mockRes();
|
|
79
|
+
|
|
80
|
+
await ApiController.deleteAll(res, storage);
|
|
81
|
+
|
|
82
|
+
const body = (res.end as jest.Mock).mock.calls[0][0] as string;
|
|
83
|
+
const payload = JSON.parse(body);
|
|
84
|
+
expect(payload.success).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("deleteOne supprime un event", async () => {
|
|
88
|
+
const res = mockRes();
|
|
89
|
+
|
|
90
|
+
await storage.save({
|
|
91
|
+
id: "xyz",
|
|
92
|
+
type: "http",
|
|
93
|
+
timestamp: Date.now(),
|
|
94
|
+
sourceIp: "127.0.0.1",
|
|
95
|
+
request: { method: "GET", path: "/", headers: {}, query: {}, body: "" },
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await ApiController.deleteOne("xyz", res, storage);
|
|
99
|
+
|
|
100
|
+
const raw = String((res.end as jest.Mock).mock.calls[0][0]);
|
|
101
|
+
const payload = JSON.parse(raw);
|
|
102
|
+
|
|
103
|
+
expect(payload.success).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("stats renvoie les stats", async () => {
|
|
107
|
+
const res = mockRes();
|
|
108
|
+
|
|
109
|
+
await ApiController.stats(res, storage);
|
|
110
|
+
|
|
111
|
+
const body = (res.end as jest.Mock).mock.calls[0][0] as string;
|
|
112
|
+
const payload = JSON.parse(body);
|
|
113
|
+
expect(payload.success).toBe(true);
|
|
114
|
+
expect(payload.stats).toEqual({ total: 0, byType: {} });
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ApiExtractor } from "../api.extractor";
|
|
2
|
+
import { IncomingMessage } from "http";
|
|
3
|
+
import { Socket } from "net";
|
|
4
|
+
import { describe, it, expect } from "@jest/globals";
|
|
5
|
+
|
|
6
|
+
function mockReq(url: string, method = "GET", body?: any): IncomingMessage {
|
|
7
|
+
const socket = new Socket();
|
|
8
|
+
const req = new IncomingMessage(socket);
|
|
9
|
+
|
|
10
|
+
req.url = url;
|
|
11
|
+
req.method = method;
|
|
12
|
+
req.headers = { host: "localhost" };
|
|
13
|
+
|
|
14
|
+
if (body !== undefined) {
|
|
15
|
+
const json = JSON.stringify(body);
|
|
16
|
+
process.nextTick(() => {
|
|
17
|
+
req.emit("data", json);
|
|
18
|
+
req.emit("end");
|
|
19
|
+
});
|
|
20
|
+
} else {
|
|
21
|
+
process.nextTick(() => req.emit("end"));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return req;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("ApiExtractor", () => {
|
|
28
|
+
it("extrait un RawEvent GET", async () => {
|
|
29
|
+
const req = mockReq("/events?page=2&type=http");
|
|
30
|
+
|
|
31
|
+
const event = await ApiExtractor.extract(req);
|
|
32
|
+
|
|
33
|
+
expect(event.method).toBe("GET");
|
|
34
|
+
expect(event.path).toBe("/events");
|
|
35
|
+
expect(event.query).toEqual({ page: "2", type: "http" });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("extrait un RawEvent POST avec body JSON", async () => {
|
|
39
|
+
const req = mockReq("/events", "POST", { hello: "world" });
|
|
40
|
+
|
|
41
|
+
const event = await ApiExtractor.extract(req);
|
|
42
|
+
|
|
43
|
+
expect(event.method).toBe("POST");
|
|
44
|
+
expect(event.body).toEqual({ hello: "world" });
|
|
45
|
+
});
|
|
46
|
+
});
|