@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,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
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
|
+
}
|