verimu 0.0.5 → 0.0.6
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/README.md +40 -2
- package/dist/cli.mjs +214 -56
- package/dist/cli.mjs.map +1 -1
- package/dist/index.cjs +214 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -14
- package/dist/index.d.ts +28 -14
- package/dist/index.mjs +214 -56
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ The NPM package for `verimu`, a tool for producing CRA-compliant SBOMs via CI /
|
|
|
11
11
|
The core scanning pipeline is CI-agnostic — it works in any environment with Node.js 20+.
|
|
12
12
|
Example CI configs are provided in the `ci-examples/` directory.
|
|
13
13
|
|
|
14
|
-
- [x] GitHub Actions (`.github/workflows/
|
|
14
|
+
- [x] GitHub Actions (`.github/workflows/release.yml`)
|
|
15
15
|
- [x] GitLab CI (`ci-examples/gitlab-ci.yml`)
|
|
16
16
|
- [x] Bitbucket Pipelines (`ci-examples/bitbucket-pipelines.yml`)
|
|
17
17
|
|
|
@@ -33,6 +33,42 @@ To run the tests, use:
|
|
|
33
33
|
npm test
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
## Releasing to npm (Tag Pipelines)
|
|
37
|
+
|
|
38
|
+
`verimu` can publish from GitHub Actions, GitLab CI, and Bitbucket Pipelines when a semver tag is pushed.
|
|
39
|
+
Each pipeline validates:
|
|
40
|
+
|
|
41
|
+
- tag is semver (`v1.2.3` or `1.2.3`)
|
|
42
|
+
- tag version matches `package.json` version
|
|
43
|
+
- tagged commit exists on `main`
|
|
44
|
+
|
|
45
|
+
### Publish credentials
|
|
46
|
+
|
|
47
|
+
- GitHub Actions (`.github/workflows/release.yml`): uses npm Trusted Publishing (OIDC), so no `NPM_TOKEN` secret is required.
|
|
48
|
+
- GitLab and Bitbucket pipelines in this repo still use `NPM_TOKEN` (`.gitlab-ci.yml`, `bitbucket-pipelines.yml`).
|
|
49
|
+
|
|
50
|
+
### Recommended release flow
|
|
51
|
+
|
|
52
|
+
1. Bump version on `main` with npm (this updates `package.json` and `package-lock.json`, then creates a git tag):
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm version patch
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
2. Push commit and tag:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
git push origin main --follow-tags
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
3. Your CI provider runs the publish job on that tag and releases to npm.
|
|
65
|
+
|
|
66
|
+
### Why this avoids version conflicts
|
|
67
|
+
|
|
68
|
+
The source of truth remains the version committed on `main`.
|
|
69
|
+
The tag is only a release trigger for that exact versioned commit.
|
|
70
|
+
You should not tag arbitrary commits with a new version string that is not already committed in `package.json`.
|
|
71
|
+
|
|
36
72
|
## Maven Scanner Notes
|
|
37
73
|
|
|
38
74
|
The Maven scanner needs resolved dependencies. Since Maven has no lockfile, it uses two strategies:
|
|
@@ -42,4 +78,6 @@ The Maven scanner needs resolved dependencies. Since Maven has no lockfile, it u
|
|
|
42
78
|
|
|
43
79
|
## Three CI / CD Pipelines as Self Check on the `verimu` package itself
|
|
44
80
|
|
|
45
|
-
There is a `bitbucket-pipelines.yml` and `.gitlab-ci.yml` in the root of the project, as well as a `.github/workflows/
|
|
81
|
+
There is a `bitbucket-pipelines.yml` and `.gitlab-ci.yml` in the root of the project, as well as a `.github/workflows/release.yml` file, all of which would run `verimu` against itself in each of the 3 frameworks we support (GitHub Actions, GitLab CI, Bitbucket Pipelines). The tests should pass in all 3 environments, confirming that `verimu` can successfully scan its own dependencies and produce a report.
|
|
82
|
+
|
|
83
|
+
The same three provider configs now also include tag-based npm release automation, so this repo is a working cross-provider reference for both scanning and publishing.
|
package/dist/cli.mjs
CHANGED
|
@@ -316,7 +316,7 @@ import { existsSync as existsSync4 } from "fs";
|
|
|
316
316
|
import path4 from "path";
|
|
317
317
|
var PipScanner = class {
|
|
318
318
|
ecosystem = "pip";
|
|
319
|
-
lockfileNames = ["
|
|
319
|
+
lockfileNames = ["Pipfile.lock", "requirements.txt"];
|
|
320
320
|
async detect(projectPath) {
|
|
321
321
|
for (const lockfile of this.lockfileNames) {
|
|
322
322
|
const fullPath = path4.join(projectPath, lockfile);
|
|
@@ -331,7 +331,7 @@ var PipScanner = class {
|
|
|
331
331
|
if (filename === "Pipfile.lock") {
|
|
332
332
|
dependencies = this.parsePipfileLock(raw, lockfilePath);
|
|
333
333
|
} else {
|
|
334
|
-
dependencies = this.parseRequirementsTxt(raw, lockfilePath);
|
|
334
|
+
dependencies = await this.parseRequirementsTxt(raw, lockfilePath);
|
|
335
335
|
}
|
|
336
336
|
return {
|
|
337
337
|
projectPath,
|
|
@@ -345,24 +345,43 @@ var PipScanner = class {
|
|
|
345
345
|
* Parses `requirements.txt` format.
|
|
346
346
|
*
|
|
347
347
|
* Supports:
|
|
348
|
-
* - `package==1.2.3` (pinned)
|
|
349
|
-
* - `package>=1.2.0` (minimum — uses the specified version)
|
|
350
|
-
* - `package~=1.2.0` (compatible release)
|
|
348
|
+
* - `package==1.2.3` (pinned) — REQUIRED
|
|
351
349
|
* - Comments (`#`) and blank lines are skipped
|
|
352
|
-
* - `-r other-file.txt` (include directive) —
|
|
350
|
+
* - `-r other-file.txt` (include directive) — recursively parsed
|
|
353
351
|
* - `--index-url` and other pip flags — skipped
|
|
352
|
+
*
|
|
353
|
+
* Throws if any dependency is not strictly pinned with ==.
|
|
354
|
+
* Use `pip freeze` to generate a properly pinned requirements.txt.
|
|
354
355
|
*/
|
|
355
|
-
parseRequirementsTxt(content, lockfilePath) {
|
|
356
|
+
async parseRequirementsTxt(content, lockfilePath, visited = /* @__PURE__ */ new Set()) {
|
|
356
357
|
const deps = [];
|
|
358
|
+
const currentDir = path4.dirname(lockfilePath);
|
|
359
|
+
const normalizedPath = path4.resolve(lockfilePath);
|
|
360
|
+
if (visited.has(normalizedPath)) {
|
|
361
|
+
return deps;
|
|
362
|
+
}
|
|
363
|
+
visited.add(normalizedPath);
|
|
357
364
|
for (const rawLine of content.split("\n")) {
|
|
358
365
|
const line = rawLine.trim();
|
|
359
|
-
if (!line || line.startsWith("#")
|
|
366
|
+
if (!line || line.startsWith("#")) {
|
|
360
367
|
continue;
|
|
361
368
|
}
|
|
362
|
-
const
|
|
363
|
-
if (
|
|
364
|
-
const
|
|
365
|
-
|
|
369
|
+
const includeMatch = line.match(/^-r\s+(.+)$/) || line.match(/^--requirement\s+(.+)$/);
|
|
370
|
+
if (includeMatch) {
|
|
371
|
+
const includePath = path4.resolve(currentDir, includeMatch[1].trim());
|
|
372
|
+
if (existsSync4(includePath)) {
|
|
373
|
+
const includeContent = await readFile4(includePath, "utf-8");
|
|
374
|
+
const includedDeps = await this.parseRequirementsTxt(includeContent, includePath, visited);
|
|
375
|
+
deps.push(...includedDeps);
|
|
376
|
+
}
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (line.startsWith("-") || line.startsWith("--")) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const pinnedMatch = line.match(/^([a-zA-Z0-9_][a-zA-Z0-9._-]*)\s*==\s*([^,\s]+)$/);
|
|
383
|
+
if (pinnedMatch) {
|
|
384
|
+
const [, name, version] = pinnedMatch;
|
|
366
385
|
if (name && version) {
|
|
367
386
|
deps.push({
|
|
368
387
|
name: this.normalizePipName(name),
|
|
@@ -373,6 +392,14 @@ var PipScanner = class {
|
|
|
373
392
|
purl: this.buildPurl(name, version)
|
|
374
393
|
});
|
|
375
394
|
}
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const depMatch = line.match(/^([a-zA-Z0-9_][a-zA-Z0-9._-]*)\s*([~=!<>].*)$/);
|
|
398
|
+
if (depMatch) {
|
|
399
|
+
throw new LockfileParseError(
|
|
400
|
+
lockfilePath,
|
|
401
|
+
`Non-pinned dependency detected: "${line}". Use pip freeze or Pipfile.lock.`
|
|
402
|
+
);
|
|
376
403
|
}
|
|
377
404
|
}
|
|
378
405
|
return deps;
|
|
@@ -431,15 +458,6 @@ var PipScanner = class {
|
|
|
431
458
|
}
|
|
432
459
|
return deps;
|
|
433
460
|
}
|
|
434
|
-
/**
|
|
435
|
-
* Extracts the version number from a pip version specifier.
|
|
436
|
-
* "1.2.3" → "1.2.3"
|
|
437
|
-
* "1.2.3,<2.0" → "1.2.3"
|
|
438
|
-
*/
|
|
439
|
-
extractVersion(spec) {
|
|
440
|
-
const cleaned = spec.split(",")[0].trim();
|
|
441
|
-
return cleaned;
|
|
442
|
-
}
|
|
443
461
|
/**
|
|
444
462
|
* Normalizes a pip package name per PEP 503.
|
|
445
463
|
* Converts to lowercase and replaces any run of [-_.] with a single hyphen.
|
|
@@ -474,61 +492,88 @@ var MavenScanner = class {
|
|
|
474
492
|
return existsSync5(pomPath) ? pomPath : null;
|
|
475
493
|
}
|
|
476
494
|
async scan(projectPath, _lockfilePath) {
|
|
495
|
+
const pomPath = path5.join(projectPath, "pom.xml");
|
|
496
|
+
const pomContent = await readFile5(pomPath, "utf-8").catch(() => null);
|
|
497
|
+
const directDeps = pomContent ? this.parsePomDependencies(pomContent) : /* @__PURE__ */ new Set();
|
|
477
498
|
const depTreePath = path5.join(projectPath, "dependency-tree.txt");
|
|
478
499
|
if (existsSync5(depTreePath)) {
|
|
479
500
|
const content = await readFile5(depTreePath, "utf-8");
|
|
480
|
-
const dependencies = this.parseDependencyList(content,
|
|
501
|
+
const dependencies = this.parseDependencyList(content, directDeps);
|
|
481
502
|
return this.buildResult(projectPath, depTreePath, dependencies);
|
|
482
503
|
}
|
|
483
504
|
if (this.isMavenAvailable()) {
|
|
484
505
|
const output = this.runMavenDependencyList(projectPath);
|
|
485
|
-
const dependencies = this.parseDependencyList(output,
|
|
486
|
-
return this.buildResult(projectPath,
|
|
506
|
+
const dependencies = this.parseDependencyList(output, directDeps);
|
|
507
|
+
return this.buildResult(projectPath, pomPath, dependencies);
|
|
487
508
|
}
|
|
488
509
|
throw new LockfileParseError(
|
|
489
|
-
|
|
510
|
+
pomPath,
|
|
490
511
|
"Maven project detected (pom.xml found) but could not resolve dependencies. Either install Maven (`mvn` must be on $PATH) or pre-generate a dependency list:\n mvn dependency:list -DoutputFile=dependency-tree.txt -DappendOutput=true"
|
|
491
512
|
);
|
|
492
513
|
}
|
|
514
|
+
/**
|
|
515
|
+
* Parses pom.xml to extract direct dependency coordinates (groupId:artifactId).
|
|
516
|
+
* This is a simple regex-based parser that handles standard dependency declarations.
|
|
517
|
+
*/
|
|
518
|
+
parsePomDependencies(pomContent) {
|
|
519
|
+
const directDeps = /* @__PURE__ */ new Set();
|
|
520
|
+
const depBlockRegex = /<dependency>\s*([\s\S]*?)<\/dependency>/g;
|
|
521
|
+
const groupIdRegex = /<groupId>\s*([^<]+)\s*<\/groupId>/;
|
|
522
|
+
const artifactIdRegex = /<artifactId>\s*([^<]+)\s*<\/artifactId>/;
|
|
523
|
+
let match;
|
|
524
|
+
while ((match = depBlockRegex.exec(pomContent)) !== null) {
|
|
525
|
+
const block = match[1];
|
|
526
|
+
const groupMatch = block.match(groupIdRegex);
|
|
527
|
+
const artifactMatch = block.match(artifactIdRegex);
|
|
528
|
+
if (groupMatch && artifactMatch) {
|
|
529
|
+
const groupId = groupMatch[1].trim();
|
|
530
|
+
const artifactId = artifactMatch[1].trim();
|
|
531
|
+
directDeps.add(`${groupId}:${artifactId}`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return directDeps;
|
|
535
|
+
}
|
|
493
536
|
/**
|
|
494
537
|
* Parses Maven `dependency:list` output.
|
|
495
538
|
*
|
|
496
539
|
* Each dependency line has the format:
|
|
497
|
-
* groupId:artifactId:type:version:scope
|
|
498
|
-
* groupId:artifactId:type:classifier:version:scope
|
|
540
|
+
* groupId:artifactId:type:version:scope (5 parts)
|
|
541
|
+
* groupId:artifactId:type:classifier:version:scope (6 parts)
|
|
499
542
|
*
|
|
500
543
|
* Lines are typically indented with leading whitespace.
|
|
501
544
|
*/
|
|
502
|
-
parseDependencyList(content,
|
|
545
|
+
parseDependencyList(content, directDeps) {
|
|
503
546
|
const deps = [];
|
|
504
|
-
const
|
|
547
|
+
const seen = /* @__PURE__ */ new Set();
|
|
505
548
|
for (const rawLine of content.split("\n")) {
|
|
506
549
|
const line = rawLine.trim();
|
|
507
550
|
if (!line) continue;
|
|
508
|
-
const
|
|
509
|
-
if (
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
if (gId && aId && ver) {
|
|
521
|
-
const name = `${gId}:${aId}`;
|
|
522
|
-
deps.push({
|
|
523
|
-
name,
|
|
524
|
-
version: ver,
|
|
525
|
-
direct: scp === "compile" || scp === "runtime" || scp === "provided",
|
|
526
|
-
ecosystem: "maven",
|
|
527
|
-
purl: this.buildPurl(gId, aId, ver)
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
}
|
|
551
|
+
const parts = line.split(":");
|
|
552
|
+
if (parts.length < 5) continue;
|
|
553
|
+
const groupId = parts[0];
|
|
554
|
+
const artifactId = parts[1];
|
|
555
|
+
let version;
|
|
556
|
+
let scope;
|
|
557
|
+
if (parts.length >= 6) {
|
|
558
|
+
version = parts[4];
|
|
559
|
+
scope = parts[5];
|
|
560
|
+
} else {
|
|
561
|
+
version = parts[3];
|
|
562
|
+
scope = parts[4];
|
|
531
563
|
}
|
|
564
|
+
if (!groupId || !artifactId || !version) continue;
|
|
565
|
+
if (scope === "test") continue;
|
|
566
|
+
const name = `${groupId}:${artifactId}`;
|
|
567
|
+
if (seen.has(name)) continue;
|
|
568
|
+
seen.add(name);
|
|
569
|
+
const isDirect = directDeps.has(name);
|
|
570
|
+
deps.push({
|
|
571
|
+
name,
|
|
572
|
+
version,
|
|
573
|
+
direct: isDirect,
|
|
574
|
+
ecosystem: "maven",
|
|
575
|
+
purl: this.buildPurl(groupId, artifactId, version)
|
|
576
|
+
});
|
|
532
577
|
}
|
|
533
578
|
return deps;
|
|
534
579
|
}
|
|
@@ -1054,7 +1099,10 @@ var OsvSource = class {
|
|
|
1054
1099
|
}
|
|
1055
1100
|
return allVulns;
|
|
1056
1101
|
}
|
|
1057
|
-
/**
|
|
1102
|
+
/**
|
|
1103
|
+
* Uses OSV's /querybatch endpoint to get vulnerability IDs,
|
|
1104
|
+
* then fetches full details for each unique vulnerability.
|
|
1105
|
+
*/
|
|
1058
1106
|
async queryBatch(dependencies) {
|
|
1059
1107
|
const queries = dependencies.map((dep) => ({
|
|
1060
1108
|
version: dep.version,
|
|
@@ -1072,18 +1120,65 @@ var OsvSource = class {
|
|
|
1072
1120
|
throw new Error(`OSV API error: ${response.status} ${response.statusText}`);
|
|
1073
1121
|
}
|
|
1074
1122
|
const data = await response.json();
|
|
1075
|
-
const
|
|
1123
|
+
const vulnIdToDeps = /* @__PURE__ */ new Map();
|
|
1076
1124
|
for (let i = 0; i < data.results.length; i++) {
|
|
1077
1125
|
const result = data.results[i];
|
|
1078
1126
|
const dep = dependencies[i];
|
|
1079
1127
|
if (result.vulns && result.vulns.length > 0) {
|
|
1080
1128
|
for (const vuln of result.vulns) {
|
|
1081
|
-
|
|
1129
|
+
const existing = vulnIdToDeps.get(vuln.id);
|
|
1130
|
+
if (existing) {
|
|
1131
|
+
existing.push(dep);
|
|
1132
|
+
} else {
|
|
1133
|
+
vulnIdToDeps.set(vuln.id, [dep]);
|
|
1134
|
+
}
|
|
1082
1135
|
}
|
|
1083
1136
|
}
|
|
1084
1137
|
}
|
|
1138
|
+
if (vulnIdToDeps.size === 0) {
|
|
1139
|
+
return [];
|
|
1140
|
+
}
|
|
1141
|
+
const vulnIds = Array.from(vulnIdToDeps.keys());
|
|
1142
|
+
const fullVulns = await this.fetchVulnerabilityDetails(vulnIds);
|
|
1143
|
+
const vulnerabilities = [];
|
|
1144
|
+
for (const osvVuln of fullVulns) {
|
|
1145
|
+
const affectedDeps = vulnIdToDeps.get(osvVuln.id) ?? [];
|
|
1146
|
+
for (const dep of affectedDeps) {
|
|
1147
|
+
vulnerabilities.push(this.mapVulnerability(osvVuln, dep));
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1085
1150
|
return vulnerabilities;
|
|
1086
1151
|
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Fetches full vulnerability details from /v1/vulns/{id} for each ID.
|
|
1154
|
+
* Makes parallel requests for efficiency.
|
|
1155
|
+
*/
|
|
1156
|
+
async fetchVulnerabilityDetails(vulnIds) {
|
|
1157
|
+
const results = [];
|
|
1158
|
+
const CONCURRENCY = 10;
|
|
1159
|
+
for (let i = 0; i < vulnIds.length; i += CONCURRENCY) {
|
|
1160
|
+
const batch = vulnIds.slice(i, i + CONCURRENCY);
|
|
1161
|
+
const promises = batch.map(async (id) => {
|
|
1162
|
+
try {
|
|
1163
|
+
const response = await this.fetchFn(`${OSV_API_BASE}/vulns/${encodeURIComponent(id)}`, {
|
|
1164
|
+
method: "GET",
|
|
1165
|
+
headers: { "Content-Type": "application/json" }
|
|
1166
|
+
});
|
|
1167
|
+
if (!response.ok) {
|
|
1168
|
+
console.warn(`Failed to fetch vulnerability ${id}: ${response.status}`);
|
|
1169
|
+
return null;
|
|
1170
|
+
}
|
|
1171
|
+
return await response.json();
|
|
1172
|
+
} catch (err) {
|
|
1173
|
+
console.warn(`Error fetching vulnerability ${id}:`, err);
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
const batchResults = await Promise.all(promises);
|
|
1178
|
+
results.push(...batchResults.filter((v) => v !== null));
|
|
1179
|
+
}
|
|
1180
|
+
return results;
|
|
1181
|
+
}
|
|
1087
1182
|
/** Maps an OSV vulnerability record to our Vulnerability type */
|
|
1088
1183
|
mapVulnerability(osvVuln, dep) {
|
|
1089
1184
|
const cveId = this.extractCveId(osvVuln);
|
|
@@ -1134,12 +1229,75 @@ var OsvSource = class {
|
|
|
1134
1229
|
}
|
|
1135
1230
|
return { level: "UNKNOWN" };
|
|
1136
1231
|
}
|
|
1137
|
-
/** Parses CVSS v3 vector string to extract the base score */
|
|
1232
|
+
/** Parses CVSS v3 vector string to extract/calculate the base score */
|
|
1138
1233
|
parseCvssScore(vectorOrScore) {
|
|
1139
1234
|
const num = parseFloat(vectorOrScore);
|
|
1140
1235
|
if (!isNaN(num) && num >= 0 && num <= 10) return num;
|
|
1236
|
+
if (vectorOrScore.startsWith("CVSS:3")) {
|
|
1237
|
+
return this.calculateCvss3Score(vectorOrScore);
|
|
1238
|
+
}
|
|
1141
1239
|
return null;
|
|
1142
1240
|
}
|
|
1241
|
+
/** Calculate CVSS v3.x base score from vector string */
|
|
1242
|
+
calculateCvss3Score(vector) {
|
|
1243
|
+
const metricValues = {
|
|
1244
|
+
AV: { N: 0.85, A: 0.62, L: 0.55, P: 0.2 },
|
|
1245
|
+
// Attack Vector
|
|
1246
|
+
AC: { L: 0.77, H: 0.44 },
|
|
1247
|
+
// Attack Complexity
|
|
1248
|
+
PR: {
|
|
1249
|
+
// Privileges Required (varies by Scope)
|
|
1250
|
+
N_U: 0.85,
|
|
1251
|
+
L_U: 0.62,
|
|
1252
|
+
H_U: 0.27,
|
|
1253
|
+
N_C: 0.85,
|
|
1254
|
+
L_C: 0.68,
|
|
1255
|
+
H_C: 0.5
|
|
1256
|
+
},
|
|
1257
|
+
UI: { N: 0.85, R: 0.62 },
|
|
1258
|
+
// User Interaction
|
|
1259
|
+
C: { H: 0.56, L: 0.22, N: 0 },
|
|
1260
|
+
// Confidentiality Impact
|
|
1261
|
+
I: { H: 0.56, L: 0.22, N: 0 },
|
|
1262
|
+
// Integrity Impact
|
|
1263
|
+
A: { H: 0.56, L: 0.22, N: 0 }
|
|
1264
|
+
// Availability Impact
|
|
1265
|
+
};
|
|
1266
|
+
const parts = vector.split("/");
|
|
1267
|
+
const metrics = {};
|
|
1268
|
+
for (const part of parts) {
|
|
1269
|
+
const [key, value] = part.split(":");
|
|
1270
|
+
if (key && value) metrics[key] = value;
|
|
1271
|
+
}
|
|
1272
|
+
const av = metricValues.AV[metrics.AV];
|
|
1273
|
+
const ac = metricValues.AC[metrics.AC];
|
|
1274
|
+
const ui = metricValues.UI[metrics.UI];
|
|
1275
|
+
const scope = metrics.S;
|
|
1276
|
+
const c = metricValues.C[metrics.C];
|
|
1277
|
+
const i = metricValues.I[metrics.I];
|
|
1278
|
+
const a = metricValues.A[metrics.A];
|
|
1279
|
+
const prKey = `${metrics.PR}_${scope}`;
|
|
1280
|
+
const pr = metricValues.PR[prKey];
|
|
1281
|
+
if ([av, ac, pr, ui, c, i, a].some((v) => v === void 0)) {
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
const iss = 1 - (1 - c) * (1 - i) * (1 - a);
|
|
1285
|
+
let impact;
|
|
1286
|
+
if (scope === "U") {
|
|
1287
|
+
impact = 6.42 * iss;
|
|
1288
|
+
} else {
|
|
1289
|
+
impact = 7.52 * (iss - 0.029) - 3.25 * Math.pow(iss - 0.02, 15);
|
|
1290
|
+
}
|
|
1291
|
+
const exploitability = 8.22 * av * ac * pr * ui;
|
|
1292
|
+
if (impact <= 0) return 0;
|
|
1293
|
+
let baseScore;
|
|
1294
|
+
if (scope === "U") {
|
|
1295
|
+
baseScore = Math.min(impact + exploitability, 10);
|
|
1296
|
+
} else {
|
|
1297
|
+
baseScore = Math.min(1.08 * (impact + exploitability), 10);
|
|
1298
|
+
}
|
|
1299
|
+
return Math.ceil(baseScore * 10) / 10;
|
|
1300
|
+
}
|
|
1143
1301
|
/** Converts a CVSS score (0-10) to a severity level */
|
|
1144
1302
|
scoreToSeverity(score) {
|
|
1145
1303
|
if (score >= 9) return "CRITICAL";
|