@j3r3mcdev/scoring 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/.github/workflows/ci.yml +29 -0
- package/.github/workflows/publish.yml +34 -0
- package/LICENSE +21 -0
- package/README.md +175 -0
- package/jest.config.js +11 -0
- package/package.json +29 -0
- package/src/core/__tests__/scoring-context.test.ts +47 -0
- package/src/core/__tests__/scoring-engine.test.ts +110 -0
- package/src/core/__tests__/scoring-result.test.ts +14 -0
- package/src/core/index.ts +8 -0
- package/src/core/scoring-context.ts +80 -0
- package/src/core/scoring-engine.ts +126 -0
- package/src/core/scoring-result.ts +15 -0
- package/src/core/scoring-types.ts +125 -0
- package/src/correlation/__tests__/chain-detector.test.ts +76 -0
- package/src/correlation/__tests__/correlator.test.ts +49 -0
- package/src/correlation/__tests__/event-grouper.test.ts +62 -0
- package/src/correlation/chain-detector.ts +99 -0
- package/src/correlation/correlator.ts +39 -0
- package/src/correlation/event-grouper.ts +47 -0
- package/src/correlation/index.ts +3 -0
- package/src/index.ts +21 -0
- package/src/normalizers/__tests__/dns.normalizer.test.ts +40 -0
- package/src/normalizers/__tests__/http.normalizer.test.ts +55 -0
- package/src/normalizers/__tests__/normalizer-registry.test.ts +89 -0
- package/src/normalizers/__tests__/waf.normalizer.test.ts +45 -0
- package/src/normalizers/dns.normalizer.ts +28 -0
- package/src/normalizers/http.normalizer.ts +53 -0
- package/src/normalizers/index.ts +34 -0
- package/src/normalizers/waf.normalizer.ts +39 -0
- package/src/reporters/__tests__/html-reporter.test.ts +51 -0
- package/src/reporters/__tests__/json-reporter.test.ts +50 -0
- package/src/reporters/__tests__/markdown-reporter.test.ts +75 -0
- package/src/reporters/__tests__/reporter-factory.test.ts +25 -0
- package/src/reporters/__tests__/reporters-integration.test.ts +46 -0
- package/src/reporters/base/BaseReporter.ts +56 -0
- package/src/reporters/base/ReporterTypes.ts +21 -0
- package/src/reporters/html/HTMLReporter.ts +240 -0
- package/src/reporters/index.ts +0 -0
- package/src/reporters/json/JSONReporter.ts +98 -0
- package/src/reporters/markdown/MarkdownReporter.ts +157 -0
- package/src/reporters/reporter-factory.ts +29 -0
- package/src/rules/__tests__/dns.rule.test.ts +42 -0
- package/src/rules/__tests__/http.rule.test.ts +46 -0
- package/src/rules/__tests__/lfi.rule.test.ts +42 -0
- package/src/rules/__tests__/path-traversal.rule.test.ts +42 -0
- package/src/rules/__tests__/rce.rule.test.ts +42 -0
- package/src/rules/__tests__/rule-registry.test.ts +40 -0
- package/src/rules/__tests__/ssrf.rule.test.ts +42 -0
- package/src/rules/__tests__/waf.rule.test.ts +40 -0
- package/src/rules/base-rule.ts +43 -0
- package/src/rules/dns.rules.ts +50 -0
- package/src/rules/http.rules.ts +72 -0
- package/src/rules/index.ts +35 -0
- package/src/rules/lfi.rule.ts +76 -0
- package/src/rules/path-transversal.rule.ts +65 -0
- package/src/rules/rce.rules.ts +73 -0
- package/src/rules/rule-registry.ts +39 -0
- package/src/rules/sqli.rules.ts +69 -0
- package/src/rules/ssrf.rules.ts +76 -0
- package/src/rules/waf.rules.ts +62 -0
- package/src/rules/xss.rules.ts +66 -0
- package/src/utils/chain-utils.ts +73 -0
- package/src/utils/date-utils.ts +80 -0
- package/src/utils/finding-utils.ts +97 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/report-utils.ts +118 -0
- package/src/utils/score-utils.ts +103 -0
- package/src/utils/string-utils.ts +54 -0
- package/src.txt +0 -0
- package/tests/scoring-engine.test.ts +7 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { normalizeEvent, Normalizers } from "../index";
|
|
2
|
+
import { HttpNormalizer } from "../http.normalizer";
|
|
3
|
+
import { DnsNormalizer } from "../dns.normalizer";
|
|
4
|
+
import { WafNormalizer } from "../waf.normalizer";
|
|
5
|
+
|
|
6
|
+
describe("NormalizerRegistry (PRO)", () => {
|
|
7
|
+
// ─────────────────────────────────────────────────────────────
|
|
8
|
+
// Vérifie que le registry expose bien les normalizers
|
|
9
|
+
// ─────────────────────────────────────────────────────────────
|
|
10
|
+
it("expose correctement les normalizers", () => {
|
|
11
|
+
expect(Normalizers.http).toBe(HttpNormalizer);
|
|
12
|
+
expect(Normalizers.dns).toBe(DnsNormalizer);
|
|
13
|
+
expect(Normalizers.waf).toBe(WafNormalizer);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// ─────────────────────────────────────────────────────────────
|
|
17
|
+
// Vérifie le dispatch dynamique pour HTTP
|
|
18
|
+
// ─────────────────────────────────────────────────────────────
|
|
19
|
+
it("normalizeEvent('http') utilise HttpNormalizer", () => {
|
|
20
|
+
const spy = jest.spyOn(HttpNormalizer, "normalize").mockReturnValue({
|
|
21
|
+
id: "123",
|
|
22
|
+
source: "http",
|
|
23
|
+
timestamp: 1,
|
|
24
|
+
payload: "test",
|
|
25
|
+
metadata: {},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const raw = { method: "GET", url: "/test" };
|
|
29
|
+
const evt = normalizeEvent("http", raw);
|
|
30
|
+
|
|
31
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
32
|
+
expect(spy).toHaveBeenCalledWith(raw);
|
|
33
|
+
expect(evt.source).toBe("http");
|
|
34
|
+
|
|
35
|
+
spy.mockRestore();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// ─────────────────────────────────────────────────────────────
|
|
39
|
+
// Vérifie le dispatch dynamique pour DNS
|
|
40
|
+
// ─────────────────────────────────────────────────────────────
|
|
41
|
+
it("normalizeEvent('dns') utilise DnsNormalizer", () => {
|
|
42
|
+
const spy = jest.spyOn(DnsNormalizer, "normalize").mockReturnValue({
|
|
43
|
+
id: "456",
|
|
44
|
+
source: "dns",
|
|
45
|
+
timestamp: 1,
|
|
46
|
+
payload: "example.com",
|
|
47
|
+
metadata: {},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const raw = { query: "example.com" };
|
|
51
|
+
const evt = normalizeEvent("dns", raw);
|
|
52
|
+
|
|
53
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
54
|
+
expect(spy).toHaveBeenCalledWith(raw);
|
|
55
|
+
expect(evt.source).toBe("dns");
|
|
56
|
+
|
|
57
|
+
spy.mockRestore();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ─────────────────────────────────────────────────────────────
|
|
61
|
+
// Vérifie le dispatch dynamique pour WAF
|
|
62
|
+
// ─────────────────────────────────────────────────────────────
|
|
63
|
+
it("normalizeEvent('waf') utilise WafNormalizer", () => {
|
|
64
|
+
const spy = jest.spyOn(WafNormalizer, "normalize").mockReturnValue({
|
|
65
|
+
id: "789",
|
|
66
|
+
source: "waf",
|
|
67
|
+
timestamp: 1,
|
|
68
|
+
payload: "WAF score: 0.9",
|
|
69
|
+
metadata: {},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const raw = { score: 0.9 };
|
|
73
|
+
const evt = normalizeEvent("waf", raw);
|
|
74
|
+
|
|
75
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
76
|
+
expect(spy).toHaveBeenCalledWith(raw);
|
|
77
|
+
expect(evt.source).toBe("waf");
|
|
78
|
+
|
|
79
|
+
spy.mockRestore();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ─────────────────────────────────────────────────────────────
|
|
83
|
+
// Vérifie qu'une source inconnue provoque une erreur claire
|
|
84
|
+
// ─────────────────────────────────────────────────────────────
|
|
85
|
+
it("lance une erreur si la source est inconnue", () => {
|
|
86
|
+
// @ts-expect-error volontaire : test d'erreur runtime
|
|
87
|
+
expect(() => normalizeEvent("unknown", {})).toThrow();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { WafNormalizer } from "../waf.normalizer";
|
|
2
|
+
|
|
3
|
+
describe("WafNormalizer (PRO)", () => {
|
|
4
|
+
it("normalise un signal WAF complet", () => {
|
|
5
|
+
const raw = {
|
|
6
|
+
score: 0.92,
|
|
7
|
+
triggeredRules: ["xss", "sqli"],
|
|
8
|
+
ip: "8.8.8.8",
|
|
9
|
+
path: "/admin",
|
|
10
|
+
method: "GET",
|
|
11
|
+
message: "Suspicious activity detected",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const evt = WafNormalizer.normalize(raw);
|
|
15
|
+
|
|
16
|
+
expect(evt.source).toBe("waf");
|
|
17
|
+
expect(evt.payload).toBe("Suspicious activity detected");
|
|
18
|
+
expect(evt.metadata.wafScore).toBe(0.92);
|
|
19
|
+
expect(evt.metadata.triggeredRules).toContain("xss");
|
|
20
|
+
expect(evt.metadata.ip).toBe("8.8.8.8");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("fallback payload si aucun message n'est fourni", () => {
|
|
24
|
+
const raw = { score: 0.5 };
|
|
25
|
+
const evt = WafNormalizer.normalize(raw);
|
|
26
|
+
|
|
27
|
+
expect(evt.payload).toBe("WAF score: 0.5");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("gère un signal WAF minimal", () => {
|
|
31
|
+
const evt = WafNormalizer.normalize({});
|
|
32
|
+
expect(evt.metadata.wafScore).toBe(0);
|
|
33
|
+
expect(evt.payload).toBe("WAF event");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("gère triggeredRules non fournis", () => {
|
|
37
|
+
const evt = WafNormalizer.normalize({ score: 0.7 });
|
|
38
|
+
expect(evt.metadata.triggeredRules).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("gère un score non numérique", () => {
|
|
42
|
+
const evt = WafNormalizer.normalize({ score: "invalid" });
|
|
43
|
+
expect(evt.metadata.wafScore).toBe("invalid");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { NormalizedEvent } from "../core/scoring-types";
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ─────────────────────────────────────────────────────────────
|
|
6
|
+
* DNS NORMALIZER — Transforme un événement DNS brut
|
|
7
|
+
* en NormalizedEvent standardisé pour le moteur de scoring.
|
|
8
|
+
* ─────────────────────────────────────────────────────────────
|
|
9
|
+
*/
|
|
10
|
+
export class DnsNormalizer {
|
|
11
|
+
static normalize(raw: any): NormalizedEvent {
|
|
12
|
+
return {
|
|
13
|
+
id: crypto.randomUUID(),
|
|
14
|
+
source: "dns",
|
|
15
|
+
timestamp: Date.now(),
|
|
16
|
+
|
|
17
|
+
// Payload utile pour les règles DNS / SSRF / Correlation
|
|
18
|
+
payload: raw.query ?? "",
|
|
19
|
+
|
|
20
|
+
metadata: {
|
|
21
|
+
ip: raw.ip ?? "unknown",
|
|
22
|
+
query: raw.query ?? "",
|
|
23
|
+
recordType: raw.type ?? "A",
|
|
24
|
+
raw: raw.raw ?? null,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { NormalizedEvent } from "../core/scoring-types";
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ─────────────────────────────────────────────────────────────
|
|
6
|
+
* HTTP NORMALIZER — Transforme une requête HTTP brute
|
|
7
|
+
* en NormalizedEvent standardisé pour le moteur de scoring.
|
|
8
|
+
* ─────────────────────────────────────────────────────────────
|
|
9
|
+
*/
|
|
10
|
+
export class HttpNormalizer {
|
|
11
|
+
static normalize(req: any): NormalizedEvent {
|
|
12
|
+
return {
|
|
13
|
+
id: crypto.randomUUID(),
|
|
14
|
+
source: "http",
|
|
15
|
+
timestamp: Date.now(),
|
|
16
|
+
|
|
17
|
+
payload: HttpNormalizer.extractPayload(req),
|
|
18
|
+
|
|
19
|
+
metadata: {
|
|
20
|
+
method: req.method ?? "UNKNOWN",
|
|
21
|
+
path: req.url ?? "/",
|
|
22
|
+
headers: req.headers ?? {},
|
|
23
|
+
ip: HttpNormalizer.extractIp(req),
|
|
24
|
+
query: req.query ?? {},
|
|
25
|
+
body: req.body ?? null,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Extraction du payload utile pour les règles (RCE, LFI, SSRF…)
|
|
32
|
+
*/
|
|
33
|
+
private static extractPayload(req: any): string {
|
|
34
|
+
const body =
|
|
35
|
+
typeof req.body === "string" ? req.body : JSON.stringify(req.body ?? "");
|
|
36
|
+
|
|
37
|
+
const query =
|
|
38
|
+
typeof req.query === "string"
|
|
39
|
+
? req.query
|
|
40
|
+
: JSON.stringify(req.query ?? "");
|
|
41
|
+
|
|
42
|
+
return `${req.method} ${req.url} | query=${query} | body=${body}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Extraction IP (X-Forwarded-For > socket)
|
|
47
|
+
*/
|
|
48
|
+
private static extractIp(req: any): string {
|
|
49
|
+
return (
|
|
50
|
+
req.headers?.["x-forwarded-for"] || req.socket?.remoteAddress || "unknown"
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { NormalizedEvent } from "../core/scoring-types";
|
|
2
|
+
import { HttpNormalizer } from "./http.normalizer";
|
|
3
|
+
import { DnsNormalizer } from "./dns.normalizer";
|
|
4
|
+
import { WafNormalizer } from "./waf.normalizer";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Type générique pour un normalizer.
|
|
8
|
+
* Tous les normalizers doivent exposer une méthode static normalize().
|
|
9
|
+
*/
|
|
10
|
+
export type Normalizer = {
|
|
11
|
+
normalize: (raw: any) => NormalizedEvent;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Registry simple des normalizers disponibles.
|
|
16
|
+
* Peut être étendu plus tard (scan, oast, smtp…)
|
|
17
|
+
*/
|
|
18
|
+
export const Normalizers = {
|
|
19
|
+
http: HttpNormalizer,
|
|
20
|
+
dns: DnsNormalizer,
|
|
21
|
+
waf: WafNormalizer,
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Helper : normalisation dynamique selon la source.
|
|
26
|
+
*/
|
|
27
|
+
export function normalizeEvent(
|
|
28
|
+
source: keyof typeof Normalizers,
|
|
29
|
+
raw: any,
|
|
30
|
+
): NormalizedEvent {
|
|
31
|
+
return Normalizers[source].normalize(raw);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { HttpNormalizer, DnsNormalizer, WafNormalizer };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { NormalizedEvent } from "../core/scoring-types";
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ─────────────────────────────────────────────────────────────
|
|
6
|
+
* WAF NORMALIZER — Transforme un signal WAF brut
|
|
7
|
+
* en NormalizedEvent standardisé pour le moteur de scoring.
|
|
8
|
+
* ─────────────────────────────────────────────────────────────
|
|
9
|
+
*/
|
|
10
|
+
export class WafNormalizer {
|
|
11
|
+
static normalize(raw: any): NormalizedEvent {
|
|
12
|
+
return {
|
|
13
|
+
id: crypto.randomUUID(),
|
|
14
|
+
source: "waf",
|
|
15
|
+
timestamp: Date.now(),
|
|
16
|
+
|
|
17
|
+
// Le payload est volontairement simple : le score ou le message
|
|
18
|
+
payload: WafNormalizer.extractPayload(raw),
|
|
19
|
+
|
|
20
|
+
metadata: {
|
|
21
|
+
wafScore: raw.score ?? 0,
|
|
22
|
+
triggeredRules: raw.triggeredRules ?? [],
|
|
23
|
+
ip: raw.ip ?? "unknown",
|
|
24
|
+
path: raw.path ?? null,
|
|
25
|
+
method: raw.method ?? null,
|
|
26
|
+
raw: raw,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Le payload doit être une string exploitable par les règles.
|
|
33
|
+
*/
|
|
34
|
+
private static extractPayload(raw: any): string {
|
|
35
|
+
if (typeof raw.message === "string") return raw.message;
|
|
36
|
+
if (typeof raw.score === "number") return `WAF score: ${raw.score}`;
|
|
37
|
+
return "WAF event";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { HTMLReporter } from "../html/HTMLReporter";
|
|
2
|
+
import { Finding, CorrelationChain } from "../../core/scoring-types";
|
|
3
|
+
|
|
4
|
+
describe("HTMLReporter", () => {
|
|
5
|
+
const reporter = new HTMLReporter();
|
|
6
|
+
|
|
7
|
+
const findings: Finding[] = [
|
|
8
|
+
{
|
|
9
|
+
id: "f1",
|
|
10
|
+
vulnerability: "xss",
|
|
11
|
+
severity: "medium",
|
|
12
|
+
score: 55,
|
|
13
|
+
details: "<b>HTML</b> injection",
|
|
14
|
+
evidence: ["<script>alert(1)</script>"] as any[],
|
|
15
|
+
chains: [],
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const chains: CorrelationChain[] = [
|
|
20
|
+
{
|
|
21
|
+
id: "c1",
|
|
22
|
+
type: "xss",
|
|
23
|
+
confidence: 0.8,
|
|
24
|
+
events: [{ id: "e1", source: "path", timestamp: 123 }] as any[],
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
it("génère un HTML valide", () => {
|
|
29
|
+
const out = reporter.generate(findings, chains);
|
|
30
|
+
|
|
31
|
+
expect(out.mime).toBe("text/html");
|
|
32
|
+
expect(out.content).toContain("<html");
|
|
33
|
+
expect(out.content).toContain("<h1>");
|
|
34
|
+
expect(out.content).toContain("Findings");
|
|
35
|
+
expect(out.content).toContain("Correlation Chains");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("échappe correctement le HTML dangereux", () => {
|
|
39
|
+
const out = reporter.generate(findings, []);
|
|
40
|
+
|
|
41
|
+
expect(out.content).toContain("<b>HTML</b>");
|
|
42
|
+
expect(out.content).toContain("<script>alert(1)</script>");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("gère un rapport vide", () => {
|
|
46
|
+
const out = reporter.generate([], []);
|
|
47
|
+
|
|
48
|
+
expect(out.content).toContain("_No findings detected._");
|
|
49
|
+
expect(out.content).toContain("_No chains detected._");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { JSONReporter } from "../json/JSONReporter";
|
|
2
|
+
import { Finding, CorrelationChain } from "../../core/scoring-types";
|
|
3
|
+
|
|
4
|
+
describe("JSONReporter", () => {
|
|
5
|
+
const reporter = new JSONReporter();
|
|
6
|
+
|
|
7
|
+
const findings: Finding[] = [
|
|
8
|
+
{
|
|
9
|
+
id: "f1",
|
|
10
|
+
vulnerability: "xss",
|
|
11
|
+
severity: "high",
|
|
12
|
+
score: 80,
|
|
13
|
+
details: "Reflected XSS detected",
|
|
14
|
+
evidence: [{ id: "ev1", source: "payload", timestamp: 789 }] as any[],
|
|
15
|
+
chains: [],
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const chains: CorrelationChain[] = [
|
|
20
|
+
{
|
|
21
|
+
id: "c1",
|
|
22
|
+
type: "xss",
|
|
23
|
+
confidence: 0.9,
|
|
24
|
+
events: [
|
|
25
|
+
{ id: "e1", source: "path", timestamp: 123 },
|
|
26
|
+
{ id: "e2", source: "path", timestamp: 456 },
|
|
27
|
+
] as any[],
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
it("génère un JSON valide", () => {
|
|
32
|
+
const out = reporter.generate(findings, chains);
|
|
33
|
+
|
|
34
|
+
expect(out.mime).toBe("application/json");
|
|
35
|
+
|
|
36
|
+
const parsed = JSON.parse(out.content);
|
|
37
|
+
|
|
38
|
+
expect(parsed.format).toBe("json");
|
|
39
|
+
expect(parsed.summary.totalFindings).toBe(1);
|
|
40
|
+
expect(parsed.summary.totalChains).toBe(1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("gère un rapport vide", () => {
|
|
44
|
+
const out = reporter.generate([], []);
|
|
45
|
+
const parsed = JSON.parse(out.content);
|
|
46
|
+
|
|
47
|
+
expect(parsed.summary.totalFindings).toBe(0);
|
|
48
|
+
expect(parsed.summary.totalChains).toBe(0);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { MarkdownReporter } from "../markdown/MarkdownReporter";
|
|
2
|
+
import { Finding, CorrelationChain } from "../../core/scoring-types";
|
|
3
|
+
|
|
4
|
+
describe("MarkdownReporter", () => {
|
|
5
|
+
const reporter = new MarkdownReporter();
|
|
6
|
+
|
|
7
|
+
const findings: Finding[] = [
|
|
8
|
+
{
|
|
9
|
+
id: "f1",
|
|
10
|
+
vulnerability: "sqli",
|
|
11
|
+
severity: "critical",
|
|
12
|
+
score: 95,
|
|
13
|
+
details: "SQL injection detected",
|
|
14
|
+
evidence: [
|
|
15
|
+
{ id: "ev1", source: "payload", timestamp: 111 },
|
|
16
|
+
{ id: "ev2", source: "db", timestamp: 222 },
|
|
17
|
+
] as any[],
|
|
18
|
+
chains: [],
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const chains: CorrelationChain[] = [
|
|
23
|
+
{
|
|
24
|
+
id: "c1",
|
|
25
|
+
type: "sqli",
|
|
26
|
+
confidence: 0.85,
|
|
27
|
+
events: [
|
|
28
|
+
{ id: "e1", source: "path", timestamp: 123 },
|
|
29
|
+
{ id: "e2", source: "query", timestamp: 456 },
|
|
30
|
+
] as any[],
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
it("génère un markdown avec les sections obligatoires", () => {
|
|
35
|
+
const out = reporter.generate(findings, chains);
|
|
36
|
+
|
|
37
|
+
expect(out.mime).toBe("text/markdown");
|
|
38
|
+
|
|
39
|
+
expect(out.content).toContain("# Security Scan Report");
|
|
40
|
+
expect(out.content).toContain("## Summary");
|
|
41
|
+
expect(out.content).toContain("## Findings");
|
|
42
|
+
expect(out.content).toContain("## Correlation Chains");
|
|
43
|
+
|
|
44
|
+
// Vérifie que la vulnérabilité apparaît bien
|
|
45
|
+
expect(out.content).toContain("SQLI");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("échappe correctement le markdown dangereux", () => {
|
|
49
|
+
const out = reporter.generate(
|
|
50
|
+
[
|
|
51
|
+
{
|
|
52
|
+
id: "f2",
|
|
53
|
+
vulnerability: "xss",
|
|
54
|
+
severity: "high",
|
|
55
|
+
score: 80,
|
|
56
|
+
details: "`code` *bold* _italic_",
|
|
57
|
+
evidence: [],
|
|
58
|
+
chains: [],
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
[],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(out.content).toContain("\\`code\\`");
|
|
65
|
+
expect(out.content).toContain("\\*bold\\*");
|
|
66
|
+
expect(out.content).toContain("\\_italic\\_");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("gère un rapport vide", () => {
|
|
70
|
+
const out = reporter.generate([], []);
|
|
71
|
+
|
|
72
|
+
expect(out.content).toContain("_No findings detected._");
|
|
73
|
+
expect(out.content).toContain("_No chains detected._");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createReporter } from "../reporter-factory";
|
|
2
|
+
import { JSONReporter } from "../json/JSONReporter";
|
|
3
|
+
import { MarkdownReporter } from "../markdown/MarkdownReporter";
|
|
4
|
+
import { HTMLReporter } from "../html/HTMLReporter";
|
|
5
|
+
|
|
6
|
+
describe("ReporterFactory", () => {
|
|
7
|
+
it("retourne un JSONReporter", () => {
|
|
8
|
+
const r = createReporter("json");
|
|
9
|
+
expect(r).toBeInstanceOf(JSONReporter);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("retourne un MarkdownReporter", () => {
|
|
13
|
+
const r = createReporter("markdown");
|
|
14
|
+
expect(r).toBeInstanceOf(MarkdownReporter);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("retourne un HTMLReporter", () => {
|
|
18
|
+
const r = createReporter("html");
|
|
19
|
+
expect(r).toBeInstanceOf(HTMLReporter);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("lève une erreur pour un format inconnu", () => {
|
|
23
|
+
expect(() => createReporter("unknown" as any)).toThrow();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createReporter } from "../reporter-factory";
|
|
2
|
+
import { Finding, CorrelationChain } from "../../core/scoring-types";
|
|
3
|
+
|
|
4
|
+
describe("Reporters Integration", () => {
|
|
5
|
+
const findings: Finding[] = [
|
|
6
|
+
{
|
|
7
|
+
id: "f1",
|
|
8
|
+
vulnerability: "sqli",
|
|
9
|
+
severity: "high",
|
|
10
|
+
score: 75,
|
|
11
|
+
details: "SQL injection",
|
|
12
|
+
evidence: [{ id: "ev1", source: "payload", timestamp: 111 }] as any[],
|
|
13
|
+
chains: [],
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const chains: CorrelationChain[] = [
|
|
18
|
+
{
|
|
19
|
+
id: "c1",
|
|
20
|
+
type: "sqli",
|
|
21
|
+
confidence: 0.9,
|
|
22
|
+
events: [{ id: "e1", source: "path", timestamp: 123 }] as any[],
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const formats = ["json", "markdown", "html"] as const;
|
|
27
|
+
|
|
28
|
+
for (const format of formats) {
|
|
29
|
+
it(`génère un rapport complet en ${format}`, () => {
|
|
30
|
+
const reporter = createReporter(format);
|
|
31
|
+
const out = reporter.generate(findings, chains);
|
|
32
|
+
|
|
33
|
+
expect(out.filename).toContain(
|
|
34
|
+
format === "markdown" ? ".md" : `.${format}`,
|
|
35
|
+
);
|
|
36
|
+
expect(out.content.length).toBeGreaterThan(50);
|
|
37
|
+
expect(out.mime).toBe(
|
|
38
|
+
format === "json"
|
|
39
|
+
? "application/json"
|
|
40
|
+
: format === "markdown"
|
|
41
|
+
? "text/markdown"
|
|
42
|
+
: "text/html",
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ─────────────────────────────────────────────────────────────
|
|
3
|
+
* BASE REPORTER — Version PRO
|
|
4
|
+
* Classe abstraite pour tous les reporters (JSON, MD, HTML)
|
|
5
|
+
* ─────────────────────────────────────────────────────────────
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Finding, CorrelationChain } from "../../core/scoring-types";
|
|
9
|
+
import { ReporterMetadata, ReporterOutput } from "./ReporterTypes";
|
|
10
|
+
|
|
11
|
+
export abstract class BaseReporter {
|
|
12
|
+
/**
|
|
13
|
+
* Nom du reporter (ex: "json", "markdown", "html").
|
|
14
|
+
*/
|
|
15
|
+
abstract getName(): string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extension du fichier généré (ex: ".json", ".md", ".html").
|
|
19
|
+
*/
|
|
20
|
+
abstract getExtension(): string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Génère le rapport final.
|
|
24
|
+
*/
|
|
25
|
+
abstract generate(
|
|
26
|
+
findings: Finding[],
|
|
27
|
+
chains: CorrelationChain[],
|
|
28
|
+
metadata?: ReporterMetadata,
|
|
29
|
+
): ReporterOutput;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Vérifie que les données d'entrée sont valides.
|
|
33
|
+
*/
|
|
34
|
+
protected validateInput(
|
|
35
|
+
findings: Finding[],
|
|
36
|
+
chains: CorrelationChain[],
|
|
37
|
+
): void {
|
|
38
|
+
if (!Array.isArray(findings)) {
|
|
39
|
+
throw new Error("findings must be an array");
|
|
40
|
+
}
|
|
41
|
+
if (!Array.isArray(chains)) {
|
|
42
|
+
throw new Error("chains must be an array");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Prépare les données avant génération (tri, normalisation…).
|
|
48
|
+
* Peut être overridé par les reporters avancés.
|
|
49
|
+
*/
|
|
50
|
+
protected prepareData(
|
|
51
|
+
findings: Finding[],
|
|
52
|
+
chains: CorrelationChain[],
|
|
53
|
+
): { findings: Finding[]; chains: CorrelationChain[] } {
|
|
54
|
+
return { findings, chains };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ─────────────────────────────────────────────────────────────
|
|
3
|
+
* REPORTER TYPES — Version PRO
|
|
4
|
+
* Types communs à tous les reporters
|
|
5
|
+
* ─────────────────────────────────────────────────────────────
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface ReporterMetadata {
|
|
9
|
+
generatedAt?: number; // timestamp
|
|
10
|
+
title?: string;
|
|
11
|
+
target?: string; // service, app, env…
|
|
12
|
+
version?: string; // version du moteur
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ReporterOutput {
|
|
16
|
+
filename: string;
|
|
17
|
+
content: string;
|
|
18
|
+
mime: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ReporterFormat = "json" | "markdown" | "html";
|