@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.
Files changed (72) hide show
  1. package/.github/workflows/ci.yml +29 -0
  2. package/.github/workflows/publish.yml +34 -0
  3. package/LICENSE +21 -0
  4. package/README.md +175 -0
  5. package/jest.config.js +11 -0
  6. package/package.json +29 -0
  7. package/src/core/__tests__/scoring-context.test.ts +47 -0
  8. package/src/core/__tests__/scoring-engine.test.ts +110 -0
  9. package/src/core/__tests__/scoring-result.test.ts +14 -0
  10. package/src/core/index.ts +8 -0
  11. package/src/core/scoring-context.ts +80 -0
  12. package/src/core/scoring-engine.ts +126 -0
  13. package/src/core/scoring-result.ts +15 -0
  14. package/src/core/scoring-types.ts +125 -0
  15. package/src/correlation/__tests__/chain-detector.test.ts +76 -0
  16. package/src/correlation/__tests__/correlator.test.ts +49 -0
  17. package/src/correlation/__tests__/event-grouper.test.ts +62 -0
  18. package/src/correlation/chain-detector.ts +99 -0
  19. package/src/correlation/correlator.ts +39 -0
  20. package/src/correlation/event-grouper.ts +47 -0
  21. package/src/correlation/index.ts +3 -0
  22. package/src/index.ts +21 -0
  23. package/src/normalizers/__tests__/dns.normalizer.test.ts +40 -0
  24. package/src/normalizers/__tests__/http.normalizer.test.ts +55 -0
  25. package/src/normalizers/__tests__/normalizer-registry.test.ts +89 -0
  26. package/src/normalizers/__tests__/waf.normalizer.test.ts +45 -0
  27. package/src/normalizers/dns.normalizer.ts +28 -0
  28. package/src/normalizers/http.normalizer.ts +53 -0
  29. package/src/normalizers/index.ts +34 -0
  30. package/src/normalizers/waf.normalizer.ts +39 -0
  31. package/src/reporters/__tests__/html-reporter.test.ts +51 -0
  32. package/src/reporters/__tests__/json-reporter.test.ts +50 -0
  33. package/src/reporters/__tests__/markdown-reporter.test.ts +75 -0
  34. package/src/reporters/__tests__/reporter-factory.test.ts +25 -0
  35. package/src/reporters/__tests__/reporters-integration.test.ts +46 -0
  36. package/src/reporters/base/BaseReporter.ts +56 -0
  37. package/src/reporters/base/ReporterTypes.ts +21 -0
  38. package/src/reporters/html/HTMLReporter.ts +240 -0
  39. package/src/reporters/index.ts +0 -0
  40. package/src/reporters/json/JSONReporter.ts +98 -0
  41. package/src/reporters/markdown/MarkdownReporter.ts +157 -0
  42. package/src/reporters/reporter-factory.ts +29 -0
  43. package/src/rules/__tests__/dns.rule.test.ts +42 -0
  44. package/src/rules/__tests__/http.rule.test.ts +46 -0
  45. package/src/rules/__tests__/lfi.rule.test.ts +42 -0
  46. package/src/rules/__tests__/path-traversal.rule.test.ts +42 -0
  47. package/src/rules/__tests__/rce.rule.test.ts +42 -0
  48. package/src/rules/__tests__/rule-registry.test.ts +40 -0
  49. package/src/rules/__tests__/ssrf.rule.test.ts +42 -0
  50. package/src/rules/__tests__/waf.rule.test.ts +40 -0
  51. package/src/rules/base-rule.ts +43 -0
  52. package/src/rules/dns.rules.ts +50 -0
  53. package/src/rules/http.rules.ts +72 -0
  54. package/src/rules/index.ts +35 -0
  55. package/src/rules/lfi.rule.ts +76 -0
  56. package/src/rules/path-transversal.rule.ts +65 -0
  57. package/src/rules/rce.rules.ts +73 -0
  58. package/src/rules/rule-registry.ts +39 -0
  59. package/src/rules/sqli.rules.ts +69 -0
  60. package/src/rules/ssrf.rules.ts +76 -0
  61. package/src/rules/waf.rules.ts +62 -0
  62. package/src/rules/xss.rules.ts +66 -0
  63. package/src/utils/chain-utils.ts +73 -0
  64. package/src/utils/date-utils.ts +80 -0
  65. package/src/utils/finding-utils.ts +97 -0
  66. package/src/utils/index.ts +6 -0
  67. package/src/utils/report-utils.ts +118 -0
  68. package/src/utils/score-utils.ts +103 -0
  69. package/src/utils/string-utils.ts +54 -0
  70. package/src.txt +0 -0
  71. package/tests/scoring-engine.test.ts +7 -0
  72. package/tsconfig.json +18 -0
@@ -0,0 +1,29 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+ pull_request:
7
+ branches: ["main"]
8
+
9
+ jobs:
10
+ build-and-test:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - name: Checkout repository
15
+ uses: actions/checkout@v4
16
+
17
+ - name: Setup Node
18
+ uses: actions/setup-node@v4
19
+ with:
20
+ node-version: 20
21
+
22
+ - name: Install dependencies
23
+ run: npm ci
24
+
25
+ - name: Run tests
26
+ run: npm test
27
+
28
+ - name: Build project
29
+ run: npm run build
@@ -0,0 +1,34 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*.*.*"
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Checkout repository
14
+ uses: actions/checkout@v4
15
+
16
+ - name: Setup Node
17
+ uses: actions/setup-node@v4
18
+ with:
19
+ node-version: 20
20
+ registry-url: "https://registry.npmjs.org"
21
+
22
+ - name: Install dependencies
23
+ run: npm ci
24
+
25
+ - name: Run tests
26
+ run: npm test
27
+
28
+ - name: Build project
29
+ run: npm run build
30
+
31
+ - name: Publish package
32
+ env:
33
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
34
+ run: npm publish --access public
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 j3r3mcdev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # @j3r3mcdev/auth-service – Scoring & Reporting Engine
2
+
3
+ Moteur de scoring, corrélation d'événements et génération de rapports (JSON, Markdown, HTML) destiné aux middlewares de sécurité, WAF applicatifs et pipelines DevSecOps.
4
+
5
+ Ce module fournit :
6
+
7
+ - un moteur de scoring normalisé
8
+ - un système de corrélation d'événements
9
+ - un format unifié pour les findings
10
+ - trois reporters professionnels (JSON, Markdown, HTML)
11
+ - une factory pour sélectionner dynamiquement un reporter
12
+ - des utilitaires de rendu sécurisés (Markdown/HTML)
13
+ - une suite de tests complète (74 tests)
14
+
15
+ ---
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install @j3r3mcdev/auth-service
21
+ ```
22
+
23
+ ## Fonctionnalités principales
24
+
25
+ ### Scoring
26
+
27
+ Score normalisé entre 0 et 100
28
+
29
+ Conversion automatique en labels (Low, Medium, High, Critical)
30
+
31
+ Informations enrichies via scoreInfo() (label, couleur, niveau de risque)
32
+
33
+ Tri automatique des findings par sévérité
34
+
35
+ ### Corrélation
36
+
37
+ Construction de chaînes d'événements (CorrelationChain)
38
+
39
+ Calcul de confiance (0–1)
40
+
41
+ Résumés lisibles via chainSummary()
42
+
43
+ Tri automatique des chaînes par confiance
44
+
45
+ ### Reporting
46
+
47
+ JSONReporter : export structuré pour CI/CD, ingestion ou stockage
48
+
49
+ MarkdownReporter : rapport lisible pour développeurs, compatible GitHub/GitLab/VSCode
50
+
51
+ HTMLReporter : rapport visuel, thème sombre, sections claires
52
+
53
+ ReporterFactory : sélection dynamique du reporter (json, markdown, html)
54
+
55
+ Génération de fichiers avec nom, MIME type et contenu final
56
+
57
+ ### Sécurité
58
+
59
+ Échappement Markdown (escapeMarkdown)
60
+
61
+ Échappement HTML (escapeHtml)
62
+
63
+ Rendu sécurisé des tableaux, listes, sections et paragraphes
64
+
65
+ Protection contre l’injection dans les rapports
66
+
67
+ ### Utilitaires
68
+
69
+ renderTable, renderList, renderSection (Markdown)
70
+
71
+ renderHtmlTable, renderHtmlList, renderHtmlParagraph (HTML)
72
+
73
+ formatIso() pour les dates
74
+
75
+ sortFindingsBySeverity() et sortChains() pour un rendu cohérent
76
+
77
+ ### Utilisation
78
+
79
+ Génération d’un rapport
80
+
81
+ ```ts
82
+ import { createReporter } from "@j3r3mcdev/auth-service";
83
+
84
+ const reporter = createReporter("markdown");
85
+
86
+ const findings = [
87
+ {
88
+ id: "f1",
89
+ vulnerability: "xss",
90
+ severity: "high",
91
+ score: 80,
92
+ details: "Reflected XSS detected",
93
+ evidence: ["<script>alert(1)</script>"],
94
+ chains: [],
95
+ },
96
+ ];
97
+
98
+ const chains = [];
99
+
100
+ const report = reporter.generate(findings, chains);
101
+
102
+ console.log(report.filename);
103
+ console.log(report.mime);
104
+ console.log(report.content);
105
+ ```
106
+
107
+ ## API
108
+
109
+ createReporter(format)
110
+
111
+ ```ts
112
+ const reporter = createReporter("json" | "markdown" | "html");
113
+ ```
114
+
115
+ Retourne une instance de :
116
+
117
+ JSONReporter
118
+ MarkdownReporter
119
+ HTMLReporter
120
+
121
+ ReporterOutput
122
+
123
+ ```ts
124
+ interface ReporterOutput {
125
+ filename: string;
126
+ content: string;
127
+ mime: string;
128
+ }
129
+ ```
130
+
131
+ ### Reporters
132
+
133
+ JSONReporter
134
+ Format structuré
135
+ Idéal pour CI/CD, ingestion, stockage
136
+
137
+ ### MarkdownReporter
138
+
139
+ Rapport lisible
140
+ Compatible GitHub/GitLab/VSCode
141
+
142
+ ### HTMLReporter
143
+
144
+ Rapport visuel
145
+ Thème sombre
146
+ Sections claires et structurées
147
+
148
+ ## Tests
149
+
150
+ La suite de tests couvre :
151
+
152
+ scoring
153
+ corrélation
154
+ reporters
155
+ factory
156
+ échappements Markdown/HTML
157
+ rapports vides
158
+ intégration complète
159
+
160
+ Exécution :
161
+
162
+ ```bash
163
+ npm run test
164
+ Résultat attendu :
165
+ ```
166
+
167
+ ```bash
168
+ Test Suites: 24 passed, 24 total
169
+ Tests: 74 passed, 74 total
170
+ Structure du projet
171
+ ```
172
+
173
+ Licence
174
+
175
+ MIT
package/jest.config.js ADDED
@@ -0,0 +1,11 @@
1
+ const { createDefaultPreset } = require("ts-jest");
2
+
3
+ const tsJestTransformCfg = createDefaultPreset().transform;
4
+
5
+ /** @type {import("jest").Config} **/
6
+ module.exports = {
7
+ testEnvironment: "node",
8
+ transform: {
9
+ ...tsJestTransformCfg,
10
+ },
11
+ };
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@j3r3mcdev/scoring",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "directories": {
7
+ "test": "tests"
8
+ },
9
+ "scripts": {
10
+ "build": "rimraf dist && tsc",
11
+ "test": "jest",
12
+ "test:watch": "jest --watch",
13
+ "clean": "rimraf dist"
14
+ },
15
+ "keywords": [],
16
+ "author": "",
17
+ "license": "ISC",
18
+ "devDependencies": {
19
+ "@types/jest": "^30.0.0",
20
+ "@typescript-eslint/eslint-plugin": "^8.60.1",
21
+ "@typescript-eslint/parser": "^8.60.1",
22
+ "eslint": "^10.4.1",
23
+ "jest": "^30.4.2",
24
+ "rimraf": "^6.1.3",
25
+ "ts-jest": "^29.4.11",
26
+ "ts-node": "^10.9.2",
27
+ "typescript": "^6.0.3"
28
+ }
29
+ }
@@ -0,0 +1,47 @@
1
+ import { ContextBuilder } from "../scoring-context";
2
+
3
+ describe("ScoringContext", () => {
4
+ test("builds an empty context", () => {
5
+ const ctx = new ContextBuilder().build();
6
+
7
+ expect(ctx.events).toEqual([]);
8
+ expect(ctx.chains).toEqual([]);
9
+ expect(ctx.metadata).toEqual({});
10
+ });
11
+
12
+ test("adds events", () => {
13
+ const event = {
14
+ id: "1",
15
+ source: "waf" as const,
16
+ timestamp: Date.now(),
17
+ metadata: {},
18
+ };
19
+
20
+ const ctx = new ContextBuilder().addEvent(event).build();
21
+
22
+ expect(ctx.events).toHaveLength(1);
23
+ expect(ctx.events[0]).toBe(event);
24
+ });
25
+
26
+ test("adds chains", () => {
27
+ const chain = {
28
+ id: "c1",
29
+ type: "ssrf" as const,
30
+ events: [],
31
+ confidence: 0.9,
32
+ };
33
+
34
+ const ctx = new ContextBuilder().addChain(chain).build();
35
+
36
+ expect(ctx.chains).toHaveLength(1);
37
+ expect(ctx.chains[0]).toBe(chain);
38
+ });
39
+
40
+ test("sets metadata", () => {
41
+ const ctx = new ContextBuilder()
42
+ .setMetadata("target", "https://example.com")
43
+ .build();
44
+
45
+ expect(ctx.metadata?.target).toBe("https://example.com");
46
+ });
47
+ });
@@ -0,0 +1,110 @@
1
+ import { ScoringEngine } from "../scoring-engine";
2
+ import { RuleRegistry } from "../../rules/rule-registry";
3
+ import { ContextBuilder } from "../scoring-context";
4
+
5
+ describe("ScoringEngine", () => {
6
+ beforeEach(() => RuleRegistry.clear());
7
+
8
+ test("returns empty result when no rules apply", () => {
9
+ const engine = new ScoringEngine();
10
+ const ctx = new ContextBuilder().build();
11
+
12
+ const result = engine.run(ctx);
13
+
14
+ expect(result.score).toBe(0);
15
+ expect(result.findings).toHaveLength(0);
16
+ });
17
+
18
+ test("executes a rule and returns findings", () => {
19
+ const fakeRule = {
20
+ id: "r1",
21
+ name: "Fake",
22
+ applies: () => true,
23
+ execute: () => [
24
+ {
25
+ ruleId: "r1",
26
+ vulnerability: "xss" as const,
27
+ score: 0.8,
28
+ severity: "high" as const,
29
+ details: "test",
30
+ },
31
+ ],
32
+ };
33
+
34
+ RuleRegistry.register(fakeRule);
35
+
36
+ const engine = new ScoringEngine();
37
+ const ctx = new ContextBuilder().build();
38
+
39
+ const result = engine.run(ctx);
40
+
41
+ expect(result.findings).toHaveLength(1);
42
+ expect(result.findings[0].vulnerability).toBe("xss");
43
+ expect(result.score).toBe(80);
44
+ expect(result.severity).toBe("high");
45
+ });
46
+
47
+ test("merges multiple findings of same vulnerability", () => {
48
+ const rule = {
49
+ id: "r1",
50
+ name: "MergeTest",
51
+ applies: () => true,
52
+ execute: () => [
53
+ {
54
+ ruleId: "r1",
55
+ vulnerability: "sqli" as const,
56
+ score: 0.4,
57
+ severity: "medium" as const,
58
+ },
59
+ {
60
+ ruleId: "r1",
61
+ vulnerability: "sqli" as const,
62
+ score: 0.9,
63
+ severity: "critical" as const,
64
+ },
65
+ ],
66
+ };
67
+
68
+ RuleRegistry.register(rule);
69
+
70
+ const engine = new ScoringEngine();
71
+ const ctx = new ContextBuilder().build();
72
+
73
+ const result = engine.run(ctx);
74
+
75
+ expect(result.findings).toHaveLength(1);
76
+ expect(result.findings[0].score).toBe(0.9);
77
+ expect(result.findings[0].severity).toBe("critical");
78
+ });
79
+
80
+ test("computes global severity as highest", () => {
81
+ const rule = {
82
+ id: "r1",
83
+ name: "MergeTest",
84
+ applies: () => true,
85
+ execute: () => [
86
+ {
87
+ ruleId: "r1",
88
+ vulnerability: "sqli" as const,
89
+ score: 0.4,
90
+ severity: "medium" as const,
91
+ },
92
+ {
93
+ ruleId: "r1",
94
+ vulnerability: "sqli" as const,
95
+ score: 0.9,
96
+ severity: "critical" as const,
97
+ },
98
+ ],
99
+ };
100
+
101
+ RuleRegistry.register(rule);
102
+
103
+ const engine = new ScoringEngine();
104
+ const ctx = new ContextBuilder().build();
105
+
106
+ const result = engine.run(ctx);
107
+
108
+ expect(result.severity).toBe("critical");
109
+ });
110
+ });
@@ -0,0 +1,14 @@
1
+ import { createEmptyResult } from "../scoring-result";
2
+
3
+ describe("createEmptyResult", () => {
4
+ test("returns a valid empty scoring result", () => {
5
+ const result = createEmptyResult();
6
+
7
+ expect(result.score).toBe(0);
8
+ expect(result.severity).toBe("low");
9
+ expect(result.findings).toEqual([]);
10
+ expect(result.chains).toEqual([]);
11
+ expect(result.metadata).toEqual({});
12
+ expect(typeof result.timestamp).toBe("number");
13
+ });
14
+ });
@@ -0,0 +1,8 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // CORE — Scoring complet
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ export * from "./scoring-types";
6
+ export * from "./scoring-context";
7
+ export * from "./scoring-result";
8
+ export * from "./scoring-engine";
@@ -0,0 +1,80 @@
1
+ import {
2
+ NormalizedEvent,
3
+ CorrelationChain,
4
+ ScoringContext,
5
+ } from "./scoring-types";
6
+
7
+ /**
8
+ * ─────────────────────────────────────────────────────────────
9
+ * SCORING CONTEXT — Version avancée
10
+ * Construit un contexte propre, stable et extensible.
11
+ * ─────────────────────────────────────────────────────────────
12
+ */
13
+
14
+ export class ContextBuilder {
15
+ private events: NormalizedEvent[] = [];
16
+ private chains: CorrelationChain[] = [];
17
+ private metadata: Record<string, any> = {};
18
+
19
+ /**
20
+ * Ajoute un événement normalisé au contexte.
21
+ */
22
+ addEvent(event: NormalizedEvent): this {
23
+ this.events.push(event);
24
+ return this;
25
+ }
26
+
27
+ /**
28
+ * Ajoute plusieurs événements d'un coup.
29
+ */
30
+ addEvents(events: NormalizedEvent[]): this {
31
+ this.events.push(...events);
32
+ return this;
33
+ }
34
+
35
+ /**
36
+ * Ajoute une chaîne de corrélation.
37
+ */
38
+ addChain(chain: CorrelationChain): this {
39
+ this.chains.push(chain);
40
+ return this;
41
+ }
42
+
43
+ /**
44
+ * Ajoute plusieurs chaînes.
45
+ */
46
+ addChains(chains: CorrelationChain[]): this {
47
+ this.chains.push(...chains);
48
+ return this;
49
+ }
50
+
51
+ /**
52
+ * Ajoute des métadonnées arbitraires (ex: URL cible, IP, user-agent…)
53
+ */
54
+ setMetadata(key: string, value: any): this {
55
+ this.metadata[key] = value;
56
+ return this;
57
+ }
58
+
59
+ /**
60
+ * Construit le contexte final.
61
+ */
62
+ build(): ScoringContext {
63
+ return {
64
+ events: [...this.events],
65
+ chains: [...this.chains],
66
+ metadata: { ...this.metadata },
67
+ };
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Helper simple pour créer un contexte vide.
73
+ */
74
+ export function createEmptyContext(): ScoringContext {
75
+ return {
76
+ events: [],
77
+ chains: [],
78
+ metadata: {},
79
+ };
80
+ }
@@ -0,0 +1,126 @@
1
+ import {
2
+ RuleFinding,
3
+ Finding,
4
+ Severity,
5
+ ScoringContext,
6
+ ScoringResult,
7
+ } from "./scoring-types";
8
+
9
+ import { RuleRegistry } from "../rules/rule-registry";
10
+ import { createEmptyResult } from "./scoring-result";
11
+
12
+ /**
13
+ * ─────────────────────────────────────────────────────────────
14
+ * SCORING ENGINE — Version avancée
15
+ * ─────────────────────────────────────────────────────────────
16
+ */
17
+ export class ScoringEngine {
18
+ run(context: ScoringContext): ScoringResult {
19
+ const rules = RuleRegistry.getAll();
20
+ const rawFindings: RuleFinding[] = [];
21
+
22
+ // 1. Exécution des règles
23
+ for (const rule of rules) {
24
+ if (rule.applies(context)) {
25
+ const results = rule.execute(context);
26
+ rawFindings.push(...results);
27
+ }
28
+ }
29
+
30
+ // 2. Aucun finding → résultat vide propre
31
+ if (rawFindings.length === 0) {
32
+ return {
33
+ ...createEmptyResult(),
34
+ chains: context.chains,
35
+ metadata: context.metadata,
36
+ };
37
+ }
38
+
39
+ // 3. Fusion des findings
40
+ const consolidated = this.consolidateFindings(rawFindings, context);
41
+
42
+ // 4. Score global
43
+ const score = this.computeGlobalScore(consolidated);
44
+
45
+ // 5. Sévérité globale
46
+ const severity = this.computeGlobalSeverity(consolidated);
47
+
48
+ // 6. Résultat final
49
+ return {
50
+ score,
51
+ severity,
52
+ findings: consolidated,
53
+ chains: context.chains,
54
+ timestamp: Date.now(),
55
+ metadata: context.metadata,
56
+ };
57
+ }
58
+
59
+ private consolidateFindings(
60
+ raw: RuleFinding[],
61
+ context: ScoringContext,
62
+ ): Finding[] {
63
+ const map = new Map<string, Finding>();
64
+
65
+ for (const f of raw) {
66
+ if (!map.has(f.vulnerability)) {
67
+ map.set(f.vulnerability, {
68
+ id: f.vulnerability,
69
+ vulnerability: f.vulnerability,
70
+ severity: f.severity,
71
+ score: f.score,
72
+ evidence: [],
73
+ chains: [],
74
+ details: f.details,
75
+ });
76
+ } else {
77
+ const existing = map.get(f.vulnerability)!;
78
+
79
+ existing.score = Math.max(existing.score, f.score);
80
+ existing.severity = this.maxSeverity(existing.severity, f.severity);
81
+
82
+ if (f.details) {
83
+ existing.details = (existing.details ?? "") + "\n" + f.details;
84
+ }
85
+ }
86
+ }
87
+
88
+ // Ajout des events + chains filtrées
89
+ for (const finding of map.values()) {
90
+ finding.evidence = context.events;
91
+ finding.chains = context.chains.filter(
92
+ (c) => c.type === finding.vulnerability,
93
+ );
94
+ }
95
+
96
+ return [...map.values()];
97
+ }
98
+
99
+ private computeGlobalScore(findings: Finding[]): number {
100
+ const total = findings.reduce((acc, f) => acc + f.score, 0);
101
+ return Math.round((total / findings.length) * 100);
102
+ }
103
+
104
+ private computeGlobalSeverity(findings: Finding[]): Severity {
105
+ return findings
106
+ .map((f) => f.severity)
107
+ .sort((a, b) => this.severityWeight(b) - this.severityWeight(a))[0];
108
+ }
109
+
110
+ private maxSeverity(a: Severity, b: Severity): Severity {
111
+ return this.severityWeight(a) >= this.severityWeight(b) ? a : b;
112
+ }
113
+
114
+ private severityWeight(s: Severity): number {
115
+ switch (s) {
116
+ case "low":
117
+ return 1;
118
+ case "medium":
119
+ return 2;
120
+ case "high":
121
+ return 3;
122
+ case "critical":
123
+ return 4;
124
+ }
125
+ }
126
+ }