@mitre/hdf-converters 3.2.0 → 3.3.1
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 +7 -1
- package/dist/detect.d.ts.map +1 -1
- package/dist/detect.js +1 -1
- package/dist/index.d.ts +23 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2407 -405
- package/dist/index.js.map +1 -1
- package/dist/{register-all-ik8sNfNf.js → register-all-DxZN6IJ4.js} +84 -4
- package/dist/register-all-DxZN6IJ4.js.map +1 -0
- package/dist/registry.d.ts.map +1 -1
- package/package.json +9 -6
- package/dist/register-all-ik8sNfNf.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { n as detectConverter, t as registerAllFingerprints } from "./register-all-
|
|
1
|
+
import { n as detectConverter, t as registerAllFingerprints } from "./register-all-DxZN6IJ4.js";
|
|
2
2
|
import { flattenOverlays } from "@mitre/hdf-parsers";
|
|
3
|
-
import { buildCsv, buildXml, parseCsv, parseJSON, parseXml, parseXmlWithArrays, sha256, stripHtml as stripHTML } from "@mitre/hdf-utilities";
|
|
4
|
-
import { Applicability, AuthorizationStatus,
|
|
5
|
-
import { DEFAULT_COMPONENT_MANAGEMENT_NIST_TAGS, DEFAULT_REMEDIATION_NIST_TAGS, DEFAULT_STATIC_ANALYSIS_NIST_TAGS, DEFAULT_STATIC_ANALYSIS_NIST_TAGS as DEFAULT_STATIC_ANALYSIS_NIST_TAGS$1, getAwsConfigNistControlByIdentifier, getAwsConfigNistControlByName, getCCINistMappings, getCweNistControl, getNessusNistControl, getNiktoNistControl, getOwaspNistControl, getScoutsuiteNistControl, nistToCci } from "@mitre/hdf-mappings";
|
|
3
|
+
import { buildCsv, buildXml, cvssScoreToSeverity, formatTimestamp, formatTimestampSeconds, impactToSeverity, parseCsv, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, sha256, stripHtml as stripHTML, trimUtcFraction } from "@mitre/hdf-utilities";
|
|
4
|
+
import { Applicability, AuthorizationStatus, CVSSSeverity, CategorizationLevel, ControlType, Ecosystem, EvidenceType, HashAlgorithm, IdentityType, Justification, MilestoneStatus, OverrideType, PlanType, ResultStatus, TargetType, VerificationMethodEnum, Version, createDescription, createMinimalBaseline, createRequirement, createResult, severityToImpact } from "@mitre/hdf-schema";
|
|
5
|
+
import { DEFAULT_COMPONENT_MANAGEMENT_NIST_TAGS, DEFAULT_REMEDIATION_NIST_TAGS, DEFAULT_STATIC_ANALYSIS_NIST_TAGS, DEFAULT_STATIC_ANALYSIS_NIST_TAGS as DEFAULT_STATIC_ANALYSIS_NIST_TAGS$1, awsConfigMappedRevisions, getAwsConfigNistControlByIdentifier, getAwsConfigNistControlByName, getCCINistMappings, getCurrentNistRevision, getCweNistControl, getNessusNistControl, getNiktoNistControl, getOwaspNistControl, getScoutsuiteNistControl, isNistStrict, nistToCci } from "@mitre/hdf-mappings";
|
|
6
6
|
//#region shared/typescript/converterutil.ts
|
|
7
7
|
/**
|
|
8
8
|
* Shared utilities for TypeScript HDF converters.
|
|
@@ -30,7 +30,7 @@ async function inputChecksum(input) {
|
|
|
30
30
|
* Compute an Integrity object (for root-level document integrity) from raw input.
|
|
31
31
|
*
|
|
32
32
|
* Returns an Integrity with algorithm and checksum fields, suitable for
|
|
33
|
-
*
|
|
33
|
+
* HDFBaseline.integrity, HDFSystem.integrity, HDFPlan.integrity, etc.
|
|
34
34
|
*
|
|
35
35
|
* @param input - Raw input string (JSON, XML, etc.)
|
|
36
36
|
* @returns Integrity object with SHA-256 algorithm and checksum
|
|
@@ -120,7 +120,7 @@ function mapCWEToNIST(cweIDs, fallback) {
|
|
|
120
120
|
return controls.size > 0 ? [...controls].sort() : fallback;
|
|
121
121
|
}
|
|
122
122
|
/** Matches CWE identifiers like "CWE-79", "CWE 89", "cwe22". */
|
|
123
|
-
const CWE_PATTERN = /CWE[- ]?(\d+)/gi;
|
|
123
|
+
const CWE_PATTERN$1 = /CWE[- ]?(\d+)/gi;
|
|
124
124
|
/**
|
|
125
125
|
* Extract all numeric CWE IDs from text.
|
|
126
126
|
* Returns deduplicated sorted array of numeric ID strings (e.g., ["79", "89"]).
|
|
@@ -129,7 +129,7 @@ const CWE_PATTERN = /CWE[- ]?(\d+)/gi;
|
|
|
129
129
|
* @returns Sorted, deduplicated numeric CWE ID strings
|
|
130
130
|
*/
|
|
131
131
|
function extractCWEIDs(text) {
|
|
132
|
-
const matches = [...text.matchAll(CWE_PATTERN)];
|
|
132
|
+
const matches = [...text.matchAll(CWE_PATTERN$1)];
|
|
133
133
|
if (matches.length === 0) return [];
|
|
134
134
|
const ids = [...new Set(matches.map((m) => m[1]))];
|
|
135
135
|
ids.sort();
|
|
@@ -175,10 +175,59 @@ function ensureArray(value) {
|
|
|
175
175
|
*/
|
|
176
176
|
const DEFAULT_REMEDIATION_NIST_TAGS$1 = ["SI-2", "RA-5"];
|
|
177
177
|
/**
|
|
178
|
+
* Map a PURL `type` segment to the AffectedPackage `ecosystem` enum.
|
|
179
|
+
* Unknown types fall back to `generic`, which the schema enum permits
|
|
180
|
+
* as a catch-all.
|
|
181
|
+
*/
|
|
182
|
+
const PURL_TYPE_TO_ECOSYSTEM = {
|
|
183
|
+
npm: Ecosystem.Npm,
|
|
184
|
+
pypi: Ecosystem.Pypi,
|
|
185
|
+
rpm: Ecosystem.RPM,
|
|
186
|
+
deb: Ecosystem.Deb,
|
|
187
|
+
maven: Ecosystem.Maven,
|
|
188
|
+
gem: Ecosystem.Gem,
|
|
189
|
+
nuget: Ecosystem.Nuget,
|
|
190
|
+
golang: Ecosystem.Go,
|
|
191
|
+
go: Ecosystem.Go,
|
|
192
|
+
cargo: Ecosystem.Cargo
|
|
193
|
+
};
|
|
194
|
+
/**
|
|
195
|
+
* Resolve an Ecosystem from a PURL type string. Returns `generic` for
|
|
196
|
+
* unknown types so callers can keep the schema's name+version+ecosystem
|
|
197
|
+
* triple valid without inventing a synthetic ecosystem.
|
|
198
|
+
*/
|
|
199
|
+
function ecosystemFromPurlType(type) {
|
|
200
|
+
if (!type) return Ecosystem.Generic;
|
|
201
|
+
return PURL_TYPE_TO_ECOSYSTEM[type.toLowerCase()] ?? Ecosystem.Generic;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Build an Affected_Package primitive from any combination of the
|
|
205
|
+
* vocabulary the schema accepts (purl / cpe / name+version+ecosystem).
|
|
206
|
+
* Returns undefined when no identifier or full triple is present —
|
|
207
|
+
* callers should skip the entry rather than emit a schema-invalid
|
|
208
|
+
* AffectedPackage. Empty strings are treated as missing.
|
|
209
|
+
*
|
|
210
|
+
* The schema's anyOf requires at least one of:
|
|
211
|
+
* - name + version + ecosystem
|
|
212
|
+
* - purl alone
|
|
213
|
+
* - cpe alone
|
|
214
|
+
*/
|
|
215
|
+
function buildAffectedPackage$1(opts) {
|
|
216
|
+
const pkg = {};
|
|
217
|
+
if (opts.purl) pkg.purl = opts.purl;
|
|
218
|
+
if (opts.cpe) pkg.cpe = opts.cpe;
|
|
219
|
+
if (opts.name) pkg.name = opts.name;
|
|
220
|
+
if (opts.version) pkg.version = opts.version;
|
|
221
|
+
if (opts.ecosystem) pkg.ecosystem = opts.ecosystem;
|
|
222
|
+
if (opts.fixedInVersion) pkg.fixedInVersion = opts.fixedInVersion;
|
|
223
|
+
if (!Boolean(pkg.name && pkg.version && pkg.ecosystem) && !pkg.purl && !pkg.cpe) return void 0;
|
|
224
|
+
return pkg;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
178
227
|
* Build an HDF Results document from options.
|
|
179
228
|
*
|
|
180
229
|
* Eliminates the repeated boilerplate of constructing generator, tool,
|
|
181
|
-
* and assembling the top-level
|
|
230
|
+
* and assembling the top-level HDFResults in every converter. Mirrors
|
|
182
231
|
* the Go shared.BuildHDFResults() function.
|
|
183
232
|
*
|
|
184
233
|
* @returns JSON string of the HDF Results document (pretty-printed)
|
|
@@ -200,7 +249,21 @@ function buildHdfResults(opts) {
|
|
|
200
249
|
if (opts.components) hdf.components = opts.components;
|
|
201
250
|
if (opts.timestamp) hdf.timestamp = opts.timestamp;
|
|
202
251
|
if (opts.statistics) hdf.statistics = opts.statistics;
|
|
203
|
-
return
|
|
252
|
+
return serializeHdf(hdf);
|
|
253
|
+
}
|
|
254
|
+
const ISO_UTC_WITH_FRACTION = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z$/;
|
|
255
|
+
function hdfTimestampReplacer(_key, value) {
|
|
256
|
+
if (typeof value === "string" && ISO_UTC_WITH_FRACTION.test(value)) return trimUtcFraction(value);
|
|
257
|
+
return value;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Serialize an HDF document to pretty-printed JSON, normalizing Date-derived
|
|
261
|
+
* timestamps to canonical trimmed-UTC RFC3339 (byte-identical to the Go
|
|
262
|
+
* converters). Use this instead of a bare `JSON.stringify` for any HDF output
|
|
263
|
+
* so the fractional-second trim is applied consistently.
|
|
264
|
+
*/
|
|
265
|
+
function serializeHdf(doc, space = 2) {
|
|
266
|
+
return JSON.stringify(doc, hdfTimestampReplacer, space);
|
|
204
267
|
}
|
|
205
268
|
/**
|
|
206
269
|
* Captures a NIST 800-53 control identifier and its base sub-control number,
|
|
@@ -333,7 +396,24 @@ function deriveControlTypeFromTags(tags) {
|
|
|
333
396
|
*/
|
|
334
397
|
function deriveVerificationMethod(code) {
|
|
335
398
|
if (code === void 0 || code === null || code === "") return void 0;
|
|
336
|
-
return VerificationMethodEnum
|
|
399
|
+
return VerificationMethodEnum.Automated;
|
|
400
|
+
}
|
|
401
|
+
function buildNoFindingsRequirement(id, codeDesc, startTime) {
|
|
402
|
+
return {
|
|
403
|
+
id,
|
|
404
|
+
title: "No findings reported",
|
|
405
|
+
impact: 0,
|
|
406
|
+
descriptions: [{
|
|
407
|
+
label: "default",
|
|
408
|
+
data: codeDesc
|
|
409
|
+
}],
|
|
410
|
+
results: [{
|
|
411
|
+
status: ResultStatus.Passed,
|
|
412
|
+
codeDesc,
|
|
413
|
+
startTime
|
|
414
|
+
}],
|
|
415
|
+
tags: {}
|
|
416
|
+
};
|
|
337
417
|
}
|
|
338
418
|
//#endregion
|
|
339
419
|
//#region converters/legacyhdf-to-hdf/typescript/converter.ts
|
|
@@ -347,14 +427,13 @@ function deriveVerificationMethod(code) {
|
|
|
347
427
|
* - Results: snake_case → camelCase for all fields
|
|
348
428
|
* - Overlay flattening: merge overlay/wrapper baselines so every requirement has results
|
|
349
429
|
*/
|
|
350
|
-
/** Valid severity values per HDF v2.0 schema. */
|
|
351
|
-
const VALID_SEVERITIES = new Set([
|
|
430
|
+
/** Valid severity values per HDF v2.0 schema (matches the Go converter). */
|
|
431
|
+
const VALID_SEVERITIES = /* @__PURE__ */ new Set([
|
|
352
432
|
"critical",
|
|
353
433
|
"high",
|
|
354
434
|
"medium",
|
|
355
435
|
"low",
|
|
356
|
-
"informational"
|
|
357
|
-
"none"
|
|
436
|
+
"informational"
|
|
358
437
|
]);
|
|
359
438
|
/**
|
|
360
439
|
* Convert tags.severity string to a valid severity value.
|
|
@@ -366,21 +445,10 @@ function tagSeverityToSeverity(tagSeverity) {
|
|
|
366
445
|
return VALID_SEVERITIES.has(normalized) ? normalized : null;
|
|
367
446
|
}
|
|
368
447
|
/**
|
|
369
|
-
* Derive severity from numeric impact score.
|
|
370
|
-
* Follows InSpec conventions: 0.9+ critical, 0.7+ high, 0.5+ medium, >0 low, 0 none.
|
|
371
|
-
*/
|
|
372
|
-
function impactToSeverity$2(impact) {
|
|
373
|
-
if (impact >= .9) return "critical";
|
|
374
|
-
if (impact >= .7) return "high";
|
|
375
|
-
if (impact >= .5) return "medium";
|
|
376
|
-
if (impact > 0) return "low";
|
|
377
|
-
return "none";
|
|
378
|
-
}
|
|
379
|
-
/**
|
|
380
448
|
* Normalize status values from v1.0 to v2.0 format.
|
|
381
449
|
* Converts snake_case to camelCase.
|
|
382
450
|
*/
|
|
383
|
-
function normalizeStatus(status) {
|
|
451
|
+
function normalizeStatus$1(status) {
|
|
384
452
|
return {
|
|
385
453
|
"passed": "passed",
|
|
386
454
|
"failed": "failed",
|
|
@@ -425,32 +493,22 @@ function computeEffectiveStatus(impact, results) {
|
|
|
425
493
|
* Convert v1.0 result to v2.0 result.
|
|
426
494
|
* Transforms snake_case field names to camelCase.
|
|
427
495
|
*/
|
|
496
|
+
const LEGACY_ZERO_TIME = "0001-01-01T00:00:00Z";
|
|
497
|
+
function legacyStartTime(s) {
|
|
498
|
+
if (s === void 0) return LEGACY_ZERO_TIME;
|
|
499
|
+
const d = parseTimestamp(s);
|
|
500
|
+
return d ? formatTimestamp(d) : LEGACY_ZERO_TIME;
|
|
501
|
+
}
|
|
428
502
|
function convertResult(v1Result) {
|
|
429
|
-
const v2Result = { status: normalizeStatus(v1Result.status) };
|
|
430
|
-
|
|
503
|
+
const v2Result = { status: normalizeStatus$1(v1Result.status) };
|
|
504
|
+
v2Result.codeDesc = v1Result.code_desc ?? "";
|
|
431
505
|
if (v1Result.run_time !== void 0) v2Result.runTime = v1Result.run_time;
|
|
432
|
-
|
|
506
|
+
v2Result.startTime = legacyStartTime(v1Result.start_time);
|
|
433
507
|
if (v1Result.message !== void 0) v2Result.message = v1Result.message;
|
|
434
508
|
if (v1Result.exception !== void 0) v2Result.exception = v1Result.exception;
|
|
435
509
|
if (v1Result.backtrace !== void 0) v2Result.backtrace = v1Result.backtrace;
|
|
436
|
-
if (v1Result.resource_class !== void 0) v2Result.
|
|
437
|
-
if (v1Result.resource_params !== void 0) v2Result.resourceParams = v1Result.resource_params;
|
|
510
|
+
if (v1Result.resource_class !== void 0) v2Result.resource = v1Result.resource_class;
|
|
438
511
|
if (v1Result.resource_id !== void 0) v2Result.resourceId = v1Result.resource_id;
|
|
439
|
-
if (v1Result.skip_message !== void 0) v2Result.skipMessage = v1Result.skip_message;
|
|
440
|
-
const knownFields = new Set([
|
|
441
|
-
"status",
|
|
442
|
-
"code_desc",
|
|
443
|
-
"run_time",
|
|
444
|
-
"start_time",
|
|
445
|
-
"message",
|
|
446
|
-
"exception",
|
|
447
|
-
"backtrace",
|
|
448
|
-
"resource_class",
|
|
449
|
-
"resource_params",
|
|
450
|
-
"resource_id",
|
|
451
|
-
"skip_message"
|
|
452
|
-
]);
|
|
453
|
-
for (const [key, value] of Object.entries(v1Result)) if (!knownFields.has(key) && !(key in v2Result)) v2Result[key] = value;
|
|
454
512
|
return v2Result;
|
|
455
513
|
}
|
|
456
514
|
/**
|
|
@@ -463,37 +521,19 @@ function convertControl(v1Control) {
|
|
|
463
521
|
impact: v1Control.impact
|
|
464
522
|
};
|
|
465
523
|
if (v1Control.title !== void 0) v2Req.title = v1Control.title;
|
|
466
|
-
if (v1Control.desc !== void 0) v2Req.desc = v1Control.desc;
|
|
467
524
|
if (v1Control.descriptions !== void 0) v2Req.descriptions = v1Control.descriptions;
|
|
468
|
-
if (v1Control.refs !== void 0) v2Req.refs = v1Control.refs;
|
|
469
525
|
if (v1Control.tags !== void 0) v2Req.tags = v1Control.tags;
|
|
470
526
|
if (v1Control.code !== void 0) v2Req.code = v1Control.code;
|
|
471
527
|
if (v1Control.source_location !== void 0) v2Req.sourceLocation = v1Control.source_location;
|
|
472
|
-
if (v1Control.
|
|
473
|
-
if (v1Control.status !== void 0) v2Req.effectiveStatus = normalizeStatus(v1Control.status);
|
|
528
|
+
if (v1Control.status !== void 0) v2Req.effectiveStatus = normalizeStatus$1(v1Control.status);
|
|
474
529
|
if (v1Control.results && Array.isArray(v1Control.results)) v2Req.results = v1Control.results.map(convertResult);
|
|
475
530
|
if (!v2Req.effectiveStatus) v2Req.effectiveStatus = computeEffectiveStatus(v1Control.impact, v2Req.results ?? []);
|
|
476
|
-
v2Req.severity = tagSeverityToSeverity(v1Control.tags?.severity) ?? impactToSeverity
|
|
531
|
+
v2Req.severity = tagSeverityToSeverity(v1Control.tags?.severity) ?? impactToSeverity(v1Control.impact);
|
|
477
532
|
const rawNist = v1Control.tags?.nist;
|
|
478
533
|
if (Array.isArray(rawNist)) {
|
|
479
534
|
const controlType = deriveControlTypeFromTags(rawNist.filter((v) => typeof v === "string"));
|
|
480
535
|
if (controlType !== void 0) v2Req.controlType = controlType;
|
|
481
536
|
}
|
|
482
|
-
const knownFields = new Set([
|
|
483
|
-
"id",
|
|
484
|
-
"title",
|
|
485
|
-
"desc",
|
|
486
|
-
"descriptions",
|
|
487
|
-
"impact",
|
|
488
|
-
"refs",
|
|
489
|
-
"tags",
|
|
490
|
-
"code",
|
|
491
|
-
"source_location",
|
|
492
|
-
"waiver_data",
|
|
493
|
-
"results",
|
|
494
|
-
"status"
|
|
495
|
-
]);
|
|
496
|
-
for (const [key, value] of Object.entries(v1Control)) if (!knownFields.has(key) && !(key in v2Req)) v2Req[key] = value;
|
|
497
537
|
return v2Req;
|
|
498
538
|
}
|
|
499
539
|
/**
|
|
@@ -506,7 +546,7 @@ function convertGroup(v1Group) {
|
|
|
506
546
|
requirements: v1Group.controls
|
|
507
547
|
};
|
|
508
548
|
if (v1Group.title !== void 0) v2Group.title = v1Group.title;
|
|
509
|
-
const knownFields = new Set([
|
|
549
|
+
const knownFields = /* @__PURE__ */ new Set([
|
|
510
550
|
"id",
|
|
511
551
|
"title",
|
|
512
552
|
"controls"
|
|
@@ -532,7 +572,7 @@ function convertDependency(v1Dep) {
|
|
|
532
572
|
if (v1Dep.compliance !== void 0) v2Dep.compliance = v1Dep.compliance;
|
|
533
573
|
if (v1Dep.status !== void 0) v2Dep.status = v1Dep.status;
|
|
534
574
|
if (v1Dep.skip_message !== void 0) v2Dep.skipMessage = v1Dep.skip_message;
|
|
535
|
-
const knownFields = new Set([
|
|
575
|
+
const knownFields = /* @__PURE__ */ new Set([
|
|
536
576
|
"name",
|
|
537
577
|
"url",
|
|
538
578
|
"path",
|
|
@@ -561,9 +601,9 @@ function convertProfile(v1Profile) {
|
|
|
561
601
|
if (v1Profile.summary !== void 0) v2Baseline.summary = v1Profile.summary;
|
|
562
602
|
if (v1Profile.license !== void 0) v2Baseline.license = v1Profile.license;
|
|
563
603
|
if (v1Profile.copyright !== void 0) v2Baseline.copyright = v1Profile.copyright;
|
|
564
|
-
if (v1Profile.copyright_email !== void 0) v2Baseline.
|
|
565
|
-
if (v1Profile.supports
|
|
566
|
-
if (v1Profile.attributes
|
|
604
|
+
if (v1Profile.copyright_email !== void 0) v2Baseline.copyrightEmail = v1Profile.copyright_email;
|
|
605
|
+
if (v1Profile.supports?.length) v2Baseline.supports = v1Profile.supports;
|
|
606
|
+
if (v1Profile.attributes?.length) v2Baseline.inputs = v1Profile.attributes;
|
|
567
607
|
if (v1Profile.status !== void 0) v2Baseline.status = v1Profile.status;
|
|
568
608
|
if (v1Profile.sha256) v2Baseline.integrity = {
|
|
569
609
|
algorithm: "sha256",
|
|
@@ -572,30 +612,9 @@ function convertProfile(v1Profile) {
|
|
|
572
612
|
if (v1Profile.parent_profile !== void 0) v2Baseline.parentBaseline = v1Profile.parent_profile;
|
|
573
613
|
if (v1Profile.status_message !== void 0) v2Baseline.statusMessage = v1Profile.status_message;
|
|
574
614
|
if (v1Profile.skip_message !== void 0) v2Baseline.skipMessage = v1Profile.skip_message;
|
|
575
|
-
if (v1Profile.groups
|
|
615
|
+
if (v1Profile.groups?.length) v2Baseline.groups = v1Profile.groups.map(convertGroup);
|
|
576
616
|
if (v1Profile.controls && Array.isArray(v1Profile.controls)) v2Baseline.requirements = v1Profile.controls.map(convertControl);
|
|
577
|
-
if (v1Profile.depends
|
|
578
|
-
const knownFields = new Set([
|
|
579
|
-
"name",
|
|
580
|
-
"version",
|
|
581
|
-
"title",
|
|
582
|
-
"maintainer",
|
|
583
|
-
"summary",
|
|
584
|
-
"license",
|
|
585
|
-
"copyright",
|
|
586
|
-
"copyright_email",
|
|
587
|
-
"supports",
|
|
588
|
-
"attributes",
|
|
589
|
-
"groups",
|
|
590
|
-
"controls",
|
|
591
|
-
"sha256",
|
|
592
|
-
"depends",
|
|
593
|
-
"parent_profile",
|
|
594
|
-
"status",
|
|
595
|
-
"status_message",
|
|
596
|
-
"skip_message"
|
|
597
|
-
]);
|
|
598
|
-
for (const [key, value] of Object.entries(v1Profile)) if (!knownFields.has(key) && !(key in v2Baseline)) v2Baseline[key] = value;
|
|
617
|
+
if (v1Profile.depends?.length) v2Baseline.depends = v1Profile.depends.map(convertDependency);
|
|
599
618
|
return v2Baseline;
|
|
600
619
|
}
|
|
601
620
|
/**
|
|
@@ -628,17 +647,21 @@ function convertV1ToV2(v1Data) {
|
|
|
628
647
|
baselines: (v1Data.profiles || []).map(convertProfile),
|
|
629
648
|
statistics: v1Data.statistics || {}
|
|
630
649
|
};
|
|
631
|
-
if (v1Data.platform)
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
650
|
+
if (v1Data.platform) {
|
|
651
|
+
const component = {
|
|
652
|
+
type: "host",
|
|
653
|
+
name: v1Data.platform.name
|
|
654
|
+
};
|
|
655
|
+
if (v1Data.platform.target_id) {
|
|
656
|
+
component.osName = v1Data.platform.name;
|
|
657
|
+
if (v1Data.platform.release !== void 0) component.osVersion = v1Data.platform.release;
|
|
658
|
+
}
|
|
659
|
+
v2.components = [component];
|
|
660
|
+
}
|
|
638
661
|
if (v1Data.generator) v2.generator = v1Data.generator;
|
|
639
662
|
v2.tool = { name: "Heimdall Data Format v1" };
|
|
640
663
|
if (v1Data.timestamp) v2.timestamp = v1Data.timestamp;
|
|
641
|
-
const knownV1Fields = new Set([
|
|
664
|
+
const knownV1Fields = /* @__PURE__ */ new Set([
|
|
642
665
|
"version",
|
|
643
666
|
"platform",
|
|
644
667
|
"profiles",
|
|
@@ -682,18 +705,19 @@ async function convertSarifToHdf(input) {
|
|
|
682
705
|
const { items: limitedRuns, truncated: truncatedRuns } = limitArray(sarif.runs);
|
|
683
706
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
684
707
|
if (truncatedRuns) console.warn(`WARNING: Input truncated at ${limitedRuns.length} run items (original: ${sarif.runs.length})`);
|
|
708
|
+
const timestamp = /* @__PURE__ */ new Date();
|
|
685
709
|
return buildHdfResults({
|
|
686
710
|
generatorName: "sarif-to-hdf",
|
|
687
711
|
converterVersion: "1.0.0",
|
|
688
712
|
toolName: firstDriver?.name,
|
|
689
713
|
toolVersion: firstDriver?.version,
|
|
690
714
|
toolFormat: "SARIF",
|
|
691
|
-
baselines: limitedRuns.map((run) => convertRun(run, sarif.version, resultsChecksum)),
|
|
715
|
+
baselines: limitedRuns.map((run) => convertRun(run, sarif.version, resultsChecksum, timestamp)),
|
|
692
716
|
components: [],
|
|
693
|
-
timestamp
|
|
717
|
+
timestamp
|
|
694
718
|
});
|
|
695
719
|
}
|
|
696
|
-
function convertRun(run, version, resultsChecksum) {
|
|
720
|
+
function convertRun(run, version, resultsChecksum, timestamp) {
|
|
697
721
|
const ruleMap = buildRuleMap(run);
|
|
698
722
|
const { items: limitedResults, truncated: truncatedResults } = limitArray(run.results);
|
|
699
723
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
@@ -717,7 +741,14 @@ function convertRun(run, version, resultsChecksum) {
|
|
|
717
741
|
const group = groupMap.get(ruleId);
|
|
718
742
|
return convertResultGroup(ruleId, group.rule, group.results);
|
|
719
743
|
});
|
|
720
|
-
|
|
744
|
+
const baselineName = run.tool?.driver?.name || "SARIF";
|
|
745
|
+
if (requirements.length === 0) {
|
|
746
|
+
const driverName = run.tool?.driver?.name?.trim() || "";
|
|
747
|
+
const target = driverName || "SARIF analyzer";
|
|
748
|
+
const idPrefix = driverName || "sarif";
|
|
749
|
+
requirements.push(buildNoFindingsRequirement(`${idPrefix}-no-findings`, `${target} ran and reported zero findings.`, timestamp));
|
|
750
|
+
}
|
|
751
|
+
return createMinimalBaseline(baselineName, requirements, {
|
|
721
752
|
version,
|
|
722
753
|
title: "Static Analysis Results Interchange Format",
|
|
723
754
|
resultsChecksum
|
|
@@ -747,8 +778,42 @@ function convertResultGroup(ruleId, rule, sarifResults) {
|
|
|
747
778
|
const controlType = deriveControlTypeFromTags(nistControls);
|
|
748
779
|
if (controlType !== void 0) req.controlType = controlType;
|
|
749
780
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
781
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
782
|
+
const packages = [];
|
|
783
|
+
for (const sr of sarifResults) {
|
|
784
|
+
const pkg = packageFromSarifProperties(sr.properties);
|
|
785
|
+
if (!pkg) continue;
|
|
786
|
+
const key = pkg.purl ?? pkg.cpe ?? `${pkg.name ?? ""}@${pkg.version ?? ""}`;
|
|
787
|
+
if (seenKeys.has(key)) continue;
|
|
788
|
+
seenKeys.add(key);
|
|
789
|
+
packages.push(pkg);
|
|
790
|
+
}
|
|
791
|
+
if (packages.length > 0) req.affectedPackages = packages;
|
|
750
792
|
return req;
|
|
751
793
|
}
|
|
794
|
+
/**
|
|
795
|
+
* Extract an Affected_Package from a SARIF result.properties bag.
|
|
796
|
+
* Recognizes the SCA-tool convention of carrying purl / cpe /
|
|
797
|
+
* packageName+packageVersion / name+version. Returns undefined for
|
|
798
|
+
* SAST results that lack any package identity.
|
|
799
|
+
*/
|
|
800
|
+
function packageFromSarifProperties(props) {
|
|
801
|
+
if (!props) return void 0;
|
|
802
|
+
const name = props.packageName ?? props.name;
|
|
803
|
+
const version = props.packageVersion ?? props.version;
|
|
804
|
+
let ecosystem;
|
|
805
|
+
if (props.purl) ecosystem = ecosystemFromPurlType(parsePurl(props.purl)?.type);
|
|
806
|
+
else if (props.ecosystem) ecosystem = ecosystemFromPurlType(props.ecosystem);
|
|
807
|
+
else if (name && version) ecosystem = Ecosystem.Generic;
|
|
808
|
+
return buildAffectedPackage$1({
|
|
809
|
+
name,
|
|
810
|
+
version,
|
|
811
|
+
ecosystem,
|
|
812
|
+
purl: props.purl,
|
|
813
|
+
cpe: props.cpe,
|
|
814
|
+
fixedInVersion: props.fixedInVersion
|
|
815
|
+
});
|
|
816
|
+
}
|
|
752
817
|
function resolveRuleLevel(rule, results) {
|
|
753
818
|
if (rule?.defaultConfiguration?.level) return rule.defaultConfiguration.level;
|
|
754
819
|
for (const r of results) if (!r.kind || r.kind === "fail") {
|
|
@@ -961,19 +1026,29 @@ async function convertJunitToHdf(input) {
|
|
|
961
1026
|
if (!input || !input.trim()) throw new Error("Empty input");
|
|
962
1027
|
validateInputSize(input, "junit");
|
|
963
1028
|
const { suites, name } = parseJUnitXML(input);
|
|
1029
|
+
const scanTime = resolveScanTime(suites);
|
|
1030
|
+
const requirements = buildRequirements(suites, scanTime);
|
|
1031
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("junit-no-findings", `JUnit scanned ${noFindingsTarget(name, suites)} and reported zero findings.`, scanTime));
|
|
964
1032
|
return buildHdfResults({
|
|
965
|
-
generatorName: "hdf
|
|
1033
|
+
generatorName: "junit-to-hdf",
|
|
966
1034
|
converterVersion: CONVERTER_VERSION$2,
|
|
967
1035
|
toolName: "JUnit XML",
|
|
968
1036
|
toolFormat: "XML",
|
|
969
|
-
baselines: [createMinimalBaseline(name,
|
|
1037
|
+
baselines: [createMinimalBaseline(name, requirements, { resultsChecksum: await inputChecksum(input) })],
|
|
970
1038
|
components: [{
|
|
971
|
-
type:
|
|
1039
|
+
type: TargetType.Application,
|
|
972
1040
|
name
|
|
973
1041
|
}],
|
|
974
|
-
timestamp:
|
|
1042
|
+
timestamp: scanTime
|
|
975
1043
|
});
|
|
976
1044
|
}
|
|
1045
|
+
function resolveScanTime(suites) {
|
|
1046
|
+
for (const suite of suites) if (suite.timestamp) {
|
|
1047
|
+
const parsed = parseTimestamp(suite.timestamp);
|
|
1048
|
+
if (parsed) return parsed;
|
|
1049
|
+
}
|
|
1050
|
+
return /* @__PURE__ */ new Date();
|
|
1051
|
+
}
|
|
977
1052
|
function parseJUnitXML(input) {
|
|
978
1053
|
const parsed = parseXmlWithArrays(input, ARRAY_TAGS$2);
|
|
979
1054
|
if (parsed.testsuites) return {
|
|
@@ -990,7 +1065,7 @@ function parseJUnitXML(input) {
|
|
|
990
1065
|
}
|
|
991
1066
|
throw new Error("Input is not a JUnit XML document: expected <testsuites> or <testsuite> root element");
|
|
992
1067
|
}
|
|
993
|
-
function buildRequirements(suites) {
|
|
1068
|
+
function buildRequirements(suites, scanTime) {
|
|
994
1069
|
const { items: limitedSuites, truncated: truncatedSuites } = limitArray(suites);
|
|
995
1070
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
996
1071
|
if (truncatedSuites) console.warn(`WARNING: Input truncated at ${limitedSuites.length} test suite items (original: ${suites.length})`);
|
|
@@ -1000,19 +1075,21 @@ function buildRequirements(suites) {
|
|
|
1000
1075
|
const { items: limitedTestCases, truncated: truncatedTC } = limitArray(testcases);
|
|
1001
1076
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
1002
1077
|
if (truncatedTC) console.warn(`WARNING: Input truncated at ${limitedTestCases.length} test case items (original: ${testcases.length})`);
|
|
1003
|
-
for (const tc of limitedTestCases) requirements.push(testCaseToRequirement(tc,
|
|
1078
|
+
for (const tc of limitedTestCases) requirements.push(testCaseToRequirement(tc, scanTime));
|
|
1004
1079
|
}
|
|
1005
1080
|
return requirements;
|
|
1006
1081
|
}
|
|
1007
|
-
function testCaseToRequirement(tc,
|
|
1082
|
+
function testCaseToRequirement(tc, scanTime) {
|
|
1008
1083
|
const id = buildID(tc);
|
|
1009
1084
|
const { status, message } = resolveStatus(tc);
|
|
1010
|
-
const resultOptions = {
|
|
1085
|
+
const resultOptions = {
|
|
1086
|
+
codeDesc: buildCodeDesc$9(tc),
|
|
1087
|
+
startTime: scanTime
|
|
1088
|
+
};
|
|
1011
1089
|
if (tc.time) {
|
|
1012
1090
|
const parsed = parseFloat(tc.time);
|
|
1013
1091
|
if (!isNaN(parsed)) resultOptions.runTime = parsed;
|
|
1014
1092
|
}
|
|
1015
|
-
if (suiteTimestamp) resultOptions.startTime = suiteTimestamp;
|
|
1016
1093
|
const result = createResult(status, message ?? "", resultOptions);
|
|
1017
1094
|
const descriptions = [{
|
|
1018
1095
|
label: "default",
|
|
@@ -1067,6 +1144,11 @@ function buildCodeDesc$9(tc) {
|
|
|
1067
1144
|
if (tc.classname) return `${tc.classname} :: ${tc.name}`;
|
|
1068
1145
|
return tc.name;
|
|
1069
1146
|
}
|
|
1147
|
+
function noFindingsTarget(baselineName, suites) {
|
|
1148
|
+
if (baselineName && baselineName !== "JUnit Test Results") return baselineName;
|
|
1149
|
+
for (const s of suites) if (s.name) return s.name;
|
|
1150
|
+
return "JUnit test suite";
|
|
1151
|
+
}
|
|
1070
1152
|
//#endregion
|
|
1071
1153
|
//#region converters/xccdf-results-to-hdf/typescript/converter.ts
|
|
1072
1154
|
const CONVERTER_VERSION$1 = "1.0.0";
|
|
@@ -1112,6 +1194,13 @@ const STATUS_MAP = {
|
|
|
1112
1194
|
* @param input - Raw XML string (XCCDF Benchmark with TestResult, or ARF asset-report-collection)
|
|
1113
1195
|
* @returns Stringified HDF Results JSON
|
|
1114
1196
|
*/
|
|
1197
|
+
function parseStartTime(raw) {
|
|
1198
|
+
if (raw) {
|
|
1199
|
+
const t = parseTimestamp(raw);
|
|
1200
|
+
if (t) return t;
|
|
1201
|
+
}
|
|
1202
|
+
return /* @__PURE__ */ new Date();
|
|
1203
|
+
}
|
|
1115
1204
|
async function convertXccdfResultsToHdf(input) {
|
|
1116
1205
|
if (!input || !input.trim()) throw new Error("Empty input");
|
|
1117
1206
|
validateInputSize(input, "xccdf-results");
|
|
@@ -1130,21 +1219,26 @@ async function convertBenchmarkResultsToHdf(benchmark, rawInput) {
|
|
|
1130
1219
|
const { items: limitedRuleResults, truncated: truncatedRR } = limitArray(ruleResults);
|
|
1131
1220
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
1132
1221
|
if (truncatedRR) console.warn(`WARNING: Input truncated at ${limitedRuleResults.length} rule-result items (original: ${ruleResults.length})`);
|
|
1133
|
-
const
|
|
1222
|
+
const scanTime = parseStartTime(testResult["start-time"]);
|
|
1223
|
+
const requirements = limitedRuleResults.map((rr) => ruleResultToRequirement(rr, ruleIndex, scanTime));
|
|
1224
|
+
if (requirements.length === 0) {
|
|
1225
|
+
const target = xccdfTargetName(testResult, benchmark);
|
|
1226
|
+
requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, scanTime));
|
|
1227
|
+
}
|
|
1134
1228
|
const resultsChecksum = await inputChecksum(rawInput);
|
|
1135
1229
|
const baseline = createMinimalBaseline(extractText(benchmark.title) || "XCCDF Benchmark", requirements, { resultsChecksum });
|
|
1136
1230
|
const components = buildTargets(testResult);
|
|
1137
|
-
const timestamp =
|
|
1231
|
+
const timestamp = scanTime;
|
|
1138
1232
|
let durationSeconds;
|
|
1139
1233
|
if (testResult["start-time"] && testResult["end-time"]) {
|
|
1140
|
-
const start =
|
|
1141
|
-
const end =
|
|
1142
|
-
if (
|
|
1234
|
+
const start = parseTimestamp(testResult["start-time"])?.getTime();
|
|
1235
|
+
const end = parseTimestamp(testResult["end-time"])?.getTime();
|
|
1236
|
+
if (start !== void 0 && end !== void 0 && end >= start) durationSeconds = (end - start) / 1e3;
|
|
1143
1237
|
}
|
|
1144
1238
|
const hdf = {
|
|
1145
1239
|
baselines: [baseline],
|
|
1146
1240
|
generator: {
|
|
1147
|
-
name: "hdf
|
|
1241
|
+
name: "xccdf-results-to-hdf",
|
|
1148
1242
|
version: CONVERTER_VERSION$1
|
|
1149
1243
|
},
|
|
1150
1244
|
tool: {
|
|
@@ -1155,7 +1249,7 @@ async function convertBenchmarkResultsToHdf(benchmark, rawInput) {
|
|
|
1155
1249
|
timestamp
|
|
1156
1250
|
};
|
|
1157
1251
|
if (durationSeconds !== void 0) hdf.statistics = { duration: durationSeconds };
|
|
1158
|
-
return
|
|
1252
|
+
return serializeHdf(hdf);
|
|
1159
1253
|
}
|
|
1160
1254
|
async function convertArfCollection(arc, rawInput) {
|
|
1161
1255
|
const resultsChecksum = await inputChecksum(rawInput);
|
|
@@ -1172,17 +1266,25 @@ async function convertArfCollection(arc, rawInput) {
|
|
|
1172
1266
|
for (const report of arc.reports?.report ?? []) {
|
|
1173
1267
|
const testResult = report.content?.TestResult;
|
|
1174
1268
|
if (!testResult?.id) continue;
|
|
1175
|
-
if (testResult["start-time"] && !firstTimestamp)
|
|
1269
|
+
if (testResult["start-time"] && !firstTimestamp) {
|
|
1270
|
+
const t = parseTimestamp(testResult["start-time"]);
|
|
1271
|
+
if (t) firstTimestamp = t;
|
|
1272
|
+
}
|
|
1176
1273
|
if (testResult["start-time"] && testResult["end-time"]) {
|
|
1177
|
-
const start =
|
|
1178
|
-
const end =
|
|
1179
|
-
if (
|
|
1274
|
+
const start = parseTimestamp(testResult["start-time"])?.getTime();
|
|
1275
|
+
const end = parseTimestamp(testResult["end-time"])?.getTime();
|
|
1276
|
+
if (start !== void 0 && end !== void 0 && end >= start) totalDuration += (end - start) / 1e3;
|
|
1180
1277
|
}
|
|
1278
|
+
const scanTime = parseStartTime(testResult["start-time"]);
|
|
1181
1279
|
const ruleResults = testResult["rule-result"] ?? [];
|
|
1182
1280
|
const { items: limitedARFRuleResults, truncated: truncatedARFRR } = limitArray(ruleResults);
|
|
1183
1281
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
1184
1282
|
if (truncatedARFRR) console.warn(`WARNING: Input truncated at ${limitedARFRuleResults.length} rule-result items (original: ${ruleResults.length})`);
|
|
1185
|
-
const requirements = limitedARFRuleResults.map((rr) => ruleResultToRequirement(rr, ruleIndex));
|
|
1283
|
+
const requirements = limitedARFRuleResults.map((rr) => ruleResultToRequirement(rr, ruleIndex, scanTime));
|
|
1284
|
+
if (requirements.length === 0) {
|
|
1285
|
+
const target = xccdfTargetName(testResult, benchmark);
|
|
1286
|
+
requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, scanTime));
|
|
1287
|
+
}
|
|
1186
1288
|
let baselineName = "";
|
|
1187
1289
|
if (benchmark) baselineName = extractText(benchmark.title) || "";
|
|
1188
1290
|
if (!baselineName) baselineName = extractText(testResult.title) || testResult.id || "ARF Report";
|
|
@@ -1203,7 +1305,7 @@ async function convertArfCollection(arc, rawInput) {
|
|
|
1203
1305
|
const hdf = {
|
|
1204
1306
|
baselines,
|
|
1205
1307
|
generator: {
|
|
1206
|
-
name: "hdf
|
|
1308
|
+
name: "xccdf-results-to-hdf",
|
|
1207
1309
|
version: CONVERTER_VERSION$1
|
|
1208
1310
|
},
|
|
1209
1311
|
tool: {
|
|
@@ -1214,7 +1316,7 @@ async function convertArfCollection(arc, rawInput) {
|
|
|
1214
1316
|
timestamp: firstTimestamp ?? /* @__PURE__ */ new Date()
|
|
1215
1317
|
};
|
|
1216
1318
|
if (totalDuration > 0) hdf.statistics = { duration: totalDuration };
|
|
1217
|
-
return
|
|
1319
|
+
return serializeHdf(hdf);
|
|
1218
1320
|
}
|
|
1219
1321
|
/**
|
|
1220
1322
|
* Find the XCCDF Benchmark embedded in an ARF data-stream-collection.
|
|
@@ -1270,7 +1372,7 @@ function buildRuleIndex(benchmark) {
|
|
|
1270
1372
|
/**
|
|
1271
1373
|
* Convert a single <rule-result> into an EvaluatedRequirement.
|
|
1272
1374
|
*/
|
|
1273
|
-
function ruleResultToRequirement(rr, ruleIndex) {
|
|
1375
|
+
function ruleResultToRequirement(rr, ruleIndex, scanTime) {
|
|
1274
1376
|
const ruleId = rr.idref ?? "";
|
|
1275
1377
|
const ruleDef = ruleIndex.get(ruleId);
|
|
1276
1378
|
const id = extractVersion$1(ruleDef?.version) || ruleId;
|
|
@@ -1288,7 +1390,16 @@ function ruleResultToRequirement(rr, ruleIndex) {
|
|
|
1288
1390
|
label: "fix",
|
|
1289
1391
|
data: fixtext
|
|
1290
1392
|
});
|
|
1291
|
-
|
|
1393
|
+
if (descriptions.length === 0) descriptions.push({
|
|
1394
|
+
label: "default",
|
|
1395
|
+
data: ""
|
|
1396
|
+
});
|
|
1397
|
+
const status = STATUS_MAP[(rr.result ?? "").toLowerCase()] ?? ResultStatus.NotReviewed;
|
|
1398
|
+
const perRuleTime = rr.time ? parseTimestamp(rr.time) : null;
|
|
1399
|
+
const result = createResult(status, "", {
|
|
1400
|
+
codeDesc: `XCCDF rule ${id}`,
|
|
1401
|
+
startTime: perRuleTime ?? scanTime
|
|
1402
|
+
});
|
|
1292
1403
|
const tags = {};
|
|
1293
1404
|
let nistTags = [];
|
|
1294
1405
|
const cciIds = extractCCIs(rr.ident ?? ruleDef?.ident ?? []);
|
|
@@ -1303,6 +1414,22 @@ function ruleResultToRequirement(rr, ruleIndex) {
|
|
|
1303
1414
|
return req;
|
|
1304
1415
|
}
|
|
1305
1416
|
/**
|
|
1417
|
+
* Pick the most specific identifier available for a no-findings codeDesc.
|
|
1418
|
+
* Falls back through TestResult target/title, benchmark title/id, then a generic phrase.
|
|
1419
|
+
*/
|
|
1420
|
+
function xccdfTargetName(testResult, benchmark) {
|
|
1421
|
+
const tr = testResult ?? {};
|
|
1422
|
+
const target = (tr.target ?? "").trim();
|
|
1423
|
+
if (target) return target;
|
|
1424
|
+
const trTitle = extractText(tr.title).trim();
|
|
1425
|
+
if (trTitle) return trTitle;
|
|
1426
|
+
const benchTitle = extractText(benchmark?.title).trim();
|
|
1427
|
+
if (benchTitle) return benchTitle;
|
|
1428
|
+
const benchId = (benchmark?.id ?? "").trim();
|
|
1429
|
+
if (benchId) return benchId;
|
|
1430
|
+
return "the target";
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1306
1433
|
* Build Component array from TestResult metadata.
|
|
1307
1434
|
*/
|
|
1308
1435
|
function buildTargets(testResult) {
|
|
@@ -1311,7 +1438,7 @@ function buildTargets(testResult) {
|
|
|
1311
1438
|
const addresses = testResult["target-address"] ?? [];
|
|
1312
1439
|
const target = {
|
|
1313
1440
|
name: targetName,
|
|
1314
|
-
type:
|
|
1441
|
+
type: TargetType.Host,
|
|
1315
1442
|
labels: {}
|
|
1316
1443
|
};
|
|
1317
1444
|
if (addresses.length > 0) target.ipAddress = addresses[0];
|
|
@@ -1450,7 +1577,7 @@ const VULN_ATTR_ORDER = [
|
|
|
1450
1577
|
"Class",
|
|
1451
1578
|
"STIG_UUID"
|
|
1452
1579
|
];
|
|
1453
|
-
const CORE_VULN_ATTR = new Set([
|
|
1580
|
+
const CORE_VULN_ATTR = /* @__PURE__ */ new Set([
|
|
1454
1581
|
"Vuln_Num",
|
|
1455
1582
|
"Severity",
|
|
1456
1583
|
"Group_Title",
|
|
@@ -1486,7 +1613,9 @@ function parseCkl(input) {
|
|
|
1486
1613
|
webDBSite: str(a["WEB_DB_SITE"]),
|
|
1487
1614
|
webDBInstance: str(a["WEB_DB_INSTANCE"])
|
|
1488
1615
|
},
|
|
1489
|
-
stigs: istigs.map((is) => {
|
|
1616
|
+
stigs: istigs.map((is, i) => {
|
|
1617
|
+
const vulns = is.VULN ?? [];
|
|
1618
|
+
if (vulns.length === 0) throw new Error(`parse ckl: <iSTIG> block ${i + 1} contains no <VULN> rules`);
|
|
1490
1619
|
const si = is.STIG_INFO?.SI_DATA ?? [];
|
|
1491
1620
|
const siVal = (name) => str(si.find((d) => d.SID_NAME === name)?.SID_DATA);
|
|
1492
1621
|
return {
|
|
@@ -1496,7 +1625,7 @@ function parseCkl(input) {
|
|
|
1496
1625
|
releaseInfo: siVal("releaseinfo"),
|
|
1497
1626
|
uuid: siVal("uuid"),
|
|
1498
1627
|
classification: siVal("classification"),
|
|
1499
|
-
vulns:
|
|
1628
|
+
vulns: vulns.map(cklVulnToModel)
|
|
1500
1629
|
};
|
|
1501
1630
|
})
|
|
1502
1631
|
};
|
|
@@ -1506,7 +1635,7 @@ function cklVulnToModel(v) {
|
|
|
1506
1635
|
const attr = (name) => str(data.find((sd) => sd.VULN_ATTRIBUTE === name)?.ATTRIBUTE_DATA);
|
|
1507
1636
|
const ccis = [];
|
|
1508
1637
|
const extra = {};
|
|
1509
|
-
const promoted = new Set([
|
|
1638
|
+
const promoted = /* @__PURE__ */ new Set([
|
|
1510
1639
|
"CCI_REF",
|
|
1511
1640
|
"Vuln_Num",
|
|
1512
1641
|
"Severity",
|
|
@@ -1655,16 +1784,20 @@ function parseCklb(input) {
|
|
|
1655
1784
|
techArea: nz(t.technology_area),
|
|
1656
1785
|
classification: nz(t.classification)
|
|
1657
1786
|
};
|
|
1658
|
-
const stigs = doc.stigs.map((s) =>
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1787
|
+
const stigs = doc.stigs.map((s, i) => {
|
|
1788
|
+
const rules = s.rules ?? [];
|
|
1789
|
+
if (rules.length === 0) throw new Error(`parse cklb: stigs[${i}] contains no rules[]`);
|
|
1790
|
+
return {
|
|
1791
|
+
stigID: nz(s.stig_id),
|
|
1792
|
+
title: nz(s.stig_name) || nz(s.display_name),
|
|
1793
|
+
displayName: nz(s.display_name),
|
|
1794
|
+
version: nz(s.version),
|
|
1795
|
+
releaseInfo: nz(s.release_info),
|
|
1796
|
+
uuid: nz(s.uuid),
|
|
1797
|
+
referenceIdentifier: nz(s.reference_identifier),
|
|
1798
|
+
vulns: rules.map(cklbRuleToModel)
|
|
1799
|
+
};
|
|
1800
|
+
});
|
|
1668
1801
|
return {
|
|
1669
1802
|
format: "cklb",
|
|
1670
1803
|
cklbVersion: nz(doc.cklb_version),
|
|
@@ -1766,18 +1899,19 @@ const CONVERTER_VERSION = "1.0.0";
|
|
|
1766
1899
|
* applicability are omitted (the checklist format cannot substantiate them).
|
|
1767
1900
|
* Original-format metadata is stashed in extensions/tags for round-trip.
|
|
1768
1901
|
*/
|
|
1769
|
-
function checklistToHdf(cl, resultsChecksum) {
|
|
1902
|
+
function checklistToHdf(cl, resultsChecksum, generatorName) {
|
|
1903
|
+
const scanTime = /* @__PURE__ */ new Date();
|
|
1770
1904
|
const hdf = {
|
|
1771
|
-
baselines: cl.stigs.map((s) => stigToBaseline(s, resultsChecksum)),
|
|
1905
|
+
baselines: cl.stigs.map((s) => stigToBaseline(s, resultsChecksum, scanTime)),
|
|
1772
1906
|
generator: {
|
|
1773
|
-
name:
|
|
1907
|
+
name: generatorName,
|
|
1774
1908
|
version: CONVERTER_VERSION
|
|
1775
1909
|
},
|
|
1776
1910
|
tool: {
|
|
1777
1911
|
name: "DISA STIG Viewer",
|
|
1778
1912
|
format: cl.format === "cklb" ? "CKLB" : "CKL"
|
|
1779
1913
|
},
|
|
1780
|
-
timestamp:
|
|
1914
|
+
timestamp: scanTime
|
|
1781
1915
|
};
|
|
1782
1916
|
const component = assetToComponent(cl.asset);
|
|
1783
1917
|
if (component) hdf.components = [component];
|
|
@@ -1785,15 +1919,15 @@ function checklistToHdf(cl, resultsChecksum) {
|
|
|
1785
1919
|
if (Object.keys(ext).length > 0) hdf.extensions = ext;
|
|
1786
1920
|
return hdf;
|
|
1787
1921
|
}
|
|
1788
|
-
function stigToBaseline(s, resultsChecksum) {
|
|
1789
|
-
const baseline = createMinimalBaseline("STIG Checklist Scan", s.vulns.map(vulnToRequirement), { resultsChecksum });
|
|
1922
|
+
function stigToBaseline(s, resultsChecksum, scanTime) {
|
|
1923
|
+
const baseline = createMinimalBaseline("STIG Checklist Scan", s.vulns.map((v) => vulnToRequirement(v, scanTime)), { resultsChecksum });
|
|
1790
1924
|
if (s.title) baseline.title = s.title;
|
|
1791
1925
|
if (s.version) baseline.version = s.version;
|
|
1792
1926
|
const ext = baselineExtensions(s);
|
|
1793
1927
|
if (Object.keys(ext).length > 0) baseline.extensions = ext;
|
|
1794
1928
|
return baseline;
|
|
1795
1929
|
}
|
|
1796
|
-
function vulnToRequirement(v) {
|
|
1930
|
+
function vulnToRequirement(v, scanTime) {
|
|
1797
1931
|
const severity = (v.severity ?? "").toLowerCase();
|
|
1798
1932
|
const impact = severity ? severityToImpact(severity) : .5;
|
|
1799
1933
|
const descriptions = [{
|
|
@@ -1809,7 +1943,10 @@ function vulnToRequirement(v) {
|
|
|
1809
1943
|
data: stripHTML(v.fixText)
|
|
1810
1944
|
});
|
|
1811
1945
|
const message = [v.findingDetails, v.comments].map((s) => (s ?? "").trim()).filter(Boolean).join("\n\n");
|
|
1812
|
-
const result = createResult(statusToHdf(v.status), message, {
|
|
1946
|
+
const result = createResult(statusToHdf(v.status), message, {
|
|
1947
|
+
codeDesc: `STIG rule ${v.ruleVer ?? ""}`,
|
|
1948
|
+
startTime: scanTime
|
|
1949
|
+
});
|
|
1813
1950
|
const tags = {};
|
|
1814
1951
|
let nistTags = [];
|
|
1815
1952
|
if (v.ccis.length > 0) {
|
|
@@ -1835,7 +1972,7 @@ function assetToComponent(a) {
|
|
|
1835
1972
|
if (!a.hostName && !a.hostIP && !a.hostFQDN) return void 0;
|
|
1836
1973
|
const c = {
|
|
1837
1974
|
name: a.hostName || a.hostFQDN || a.hostIP || "",
|
|
1838
|
-
type:
|
|
1975
|
+
type: TargetType.Host
|
|
1839
1976
|
};
|
|
1840
1977
|
if (a.hostIP) c.ipAddress = a.hostIP;
|
|
1841
1978
|
if (a.hostFQDN) c.fqdn = a.hostFQDN;
|
|
@@ -2010,7 +2147,7 @@ async function convertCklToHdf(input) {
|
|
|
2010
2147
|
validateInputSize(input, "ckl-to-hdf");
|
|
2011
2148
|
const resultsChecksum = await inputChecksum(input);
|
|
2012
2149
|
const checklist = parseCkl(input);
|
|
2013
|
-
return JSON.stringify(checklistToHdf(checklist, resultsChecksum), null, 2);
|
|
2150
|
+
return JSON.stringify(checklistToHdf(checklist, resultsChecksum, "ckl-to-hdf"), null, 2);
|
|
2014
2151
|
}
|
|
2015
2152
|
//#endregion
|
|
2016
2153
|
//#region converters/cklb-to-hdf/typescript/converter.ts
|
|
@@ -2027,7 +2164,7 @@ async function convertCklbToHdf(input) {
|
|
|
2027
2164
|
validateInputSize(input, "cklb-to-hdf");
|
|
2028
2165
|
const resultsChecksum = await inputChecksum(input);
|
|
2029
2166
|
const checklist = parseCklb(input);
|
|
2030
|
-
return JSON.stringify(checklistToHdf(checklist, resultsChecksum), null, 2);
|
|
2167
|
+
return JSON.stringify(checklistToHdf(checklist, resultsChecksum, "cklb-to-hdf"), null, 2);
|
|
2031
2168
|
}
|
|
2032
2169
|
//#endregion
|
|
2033
2170
|
//#region converters/hdf-to-ckl/typescript/converter.ts
|
|
@@ -2078,7 +2215,28 @@ function formatDependencyPath(from) {
|
|
|
2078
2215
|
/**
|
|
2079
2216
|
* Builds a single EvaluatedRequirement from a group of vulnerabilities sharing an ID.
|
|
2080
2217
|
*/
|
|
2081
|
-
|
|
2218
|
+
/**
|
|
2219
|
+
* Map Snyk's `packageManager` value to an Affected_Package ecosystem.
|
|
2220
|
+
* Snyk reports values that don't always match PURL types one-to-one
|
|
2221
|
+
* (pip → pypi, rubygems → gem, yarn → npm). Unknown managers fall back
|
|
2222
|
+
* to `generic`.
|
|
2223
|
+
*/
|
|
2224
|
+
function ecosystemFromSnykPackageManager(pm) {
|
|
2225
|
+
if (!pm) return Ecosystem.Generic;
|
|
2226
|
+
const lower = pm.toLowerCase();
|
|
2227
|
+
if (lower === "pip" || lower === "pip3") return Ecosystem.Pypi;
|
|
2228
|
+
if (lower === "rubygems" || lower === "bundler") return Ecosystem.Gem;
|
|
2229
|
+
if (lower === "yarn" || lower === "npm") return Ecosystem.Npm;
|
|
2230
|
+
return ecosystemFromPurlType(lower);
|
|
2231
|
+
}
|
|
2232
|
+
/** Synthesize a `pkg:<type>/<name>@<version>` PURL when the ecosystem
|
|
2233
|
+
* maps cleanly. Returns undefined for `generic` so we don't emit a
|
|
2234
|
+
* fake `pkg:generic/...` PURL that downstream tools can't dereference. */
|
|
2235
|
+
function synthesizePurl(ecosystem, name, version) {
|
|
2236
|
+
if (ecosystem === Ecosystem.Generic) return void 0;
|
|
2237
|
+
return `pkg:${ecosystem}/${name}@${version}`;
|
|
2238
|
+
}
|
|
2239
|
+
function buildRequirement$16(vulnID, vulns, scanTime, packageManager) {
|
|
2082
2240
|
const rep = vulns[0];
|
|
2083
2241
|
const cweIDs = rep.identifiers.CWE ?? [];
|
|
2084
2242
|
const nist = mapCWEToNIST(cweIDs, DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
|
|
@@ -2093,17 +2251,33 @@ function buildRequirement$16(vulnID, vulns) {
|
|
|
2093
2251
|
label: "default",
|
|
2094
2252
|
data: rep.description
|
|
2095
2253
|
}];
|
|
2096
|
-
const results = vulns.map((vuln) => createResult(ResultStatus.Failed, void 0, {
|
|
2254
|
+
const results = vulns.map((vuln) => createResult(ResultStatus.Failed, void 0, {
|
|
2255
|
+
codeDesc: formatDependencyPath(vuln.from),
|
|
2256
|
+
startTime: scanTime
|
|
2257
|
+
}));
|
|
2097
2258
|
const req = createRequirement(vulnID, rep.title, descriptions, severityToImpact(rep.severity), results, { tags });
|
|
2098
2259
|
const controlType = deriveControlTypeFromTags(nist);
|
|
2099
2260
|
if (controlType !== void 0) req.controlType = controlType;
|
|
2100
2261
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
2262
|
+
const name = rep.packageName ?? rep.moduleName;
|
|
2263
|
+
const version = rep.version;
|
|
2264
|
+
if (name && version) {
|
|
2265
|
+
const ecosystem = ecosystemFromSnykPackageManager(packageManager);
|
|
2266
|
+
const pkg = buildAffectedPackage$1({
|
|
2267
|
+
name,
|
|
2268
|
+
version,
|
|
2269
|
+
ecosystem,
|
|
2270
|
+
purl: synthesizePurl(ecosystem, name, version),
|
|
2271
|
+
fixedInVersion: rep.fixedIn?.[0]
|
|
2272
|
+
});
|
|
2273
|
+
if (pkg) req.affectedPackages = [pkg];
|
|
2274
|
+
}
|
|
2101
2275
|
return req;
|
|
2102
2276
|
}
|
|
2103
2277
|
/**
|
|
2104
2278
|
* Converts a single Snyk project report to an HDF baseline.
|
|
2105
2279
|
*/
|
|
2106
|
-
function convertSingleProject(report, resultsChecksum) {
|
|
2280
|
+
function convertSingleProject(report, resultsChecksum, scanTime) {
|
|
2107
2281
|
const { items: limitedVulns, truncated: truncatedVulns } = limitArray(report.vulnerabilities);
|
|
2108
2282
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
2109
2283
|
if (truncatedVulns) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${report.vulnerabilities.length})`);
|
|
@@ -2114,7 +2288,11 @@ function convertSingleProject(report, resultsChecksum) {
|
|
|
2114
2288
|
else groups.set(vuln.id, [vuln]);
|
|
2115
2289
|
}
|
|
2116
2290
|
const requirements = [];
|
|
2117
|
-
for (const [vulnID, vulns] of groups) requirements.push(buildRequirement$16(vulnID, vulns));
|
|
2291
|
+
for (const [vulnID, vulns] of groups) requirements.push(buildRequirement$16(vulnID, vulns, scanTime, report.packageManager));
|
|
2292
|
+
if (requirements.length === 0) {
|
|
2293
|
+
const target = report.projectName ?? report.path ?? "project";
|
|
2294
|
+
requirements.push(buildNoFindingsRequirement("snyk-no-findings", `Snyk scanned ${target} and reported zero vulnerable components.`, scanTime));
|
|
2295
|
+
}
|
|
2118
2296
|
return createMinimalBaseline("Snyk Scan", requirements, {
|
|
2119
2297
|
resultsChecksum,
|
|
2120
2298
|
title: `Snyk Project: ${report.projectName ?? ""} Snyk Path: ${report.path ?? ""}`,
|
|
@@ -2137,6 +2315,7 @@ async function convertSnykToHdf(input) {
|
|
|
2137
2315
|
const detected = detectConverter(input);
|
|
2138
2316
|
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
|
|
2139
2317
|
const resultsChecksum = await inputChecksum(input);
|
|
2318
|
+
const scanTime = /* @__PURE__ */ new Date();
|
|
2140
2319
|
const parsed = parseJSON(input);
|
|
2141
2320
|
if (!parsed || typeof parsed !== "object") throw new Error("snyk: invalid JSON");
|
|
2142
2321
|
let baselines;
|
|
@@ -2145,10 +2324,10 @@ async function convertSnykToHdf(input) {
|
|
|
2145
2324
|
const { items: limitedProjects, truncated: truncatedProjects } = limitArray(parsed);
|
|
2146
2325
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
2147
2326
|
if (truncatedProjects) console.warn(`WARNING: Input truncated at ${limitedProjects.length} project items (original: ${parsed.length})`);
|
|
2148
|
-
baselines = limitedProjects.map((report) => convertSingleProject(report, resultsChecksum));
|
|
2327
|
+
baselines = limitedProjects.map((report) => convertSingleProject(report, resultsChecksum, scanTime));
|
|
2149
2328
|
targetName = limitedProjects[0]?.projectName ?? limitedProjects[0]?.path ?? "";
|
|
2150
2329
|
} else {
|
|
2151
|
-
baselines = [convertSingleProject(parsed, resultsChecksum)];
|
|
2330
|
+
baselines = [convertSingleProject(parsed, resultsChecksum, scanTime)];
|
|
2152
2331
|
targetName = parsed.projectName ?? parsed.path ?? "";
|
|
2153
2332
|
}
|
|
2154
2333
|
return buildHdfResults({
|
|
@@ -2159,14 +2338,14 @@ async function convertSnykToHdf(input) {
|
|
|
2159
2338
|
baselines,
|
|
2160
2339
|
components: [{
|
|
2161
2340
|
name: targetName,
|
|
2162
|
-
type:
|
|
2341
|
+
type: TargetType.Application
|
|
2163
2342
|
}],
|
|
2164
|
-
timestamp:
|
|
2343
|
+
timestamp: scanTime
|
|
2165
2344
|
});
|
|
2166
2345
|
}
|
|
2167
2346
|
//#endregion
|
|
2168
2347
|
//#region converters/grype-to-hdf/typescript/converter.ts
|
|
2169
|
-
const IMPACT_MAPPING$8 = new Map([
|
|
2348
|
+
const IMPACT_MAPPING$8 = /* @__PURE__ */ new Map([
|
|
2170
2349
|
["critical", .9],
|
|
2171
2350
|
["high", .7],
|
|
2172
2351
|
["medium", .5],
|
|
@@ -2227,6 +2406,92 @@ function buildCodeDesc$8(match) {
|
|
|
2227
2406
|
}
|
|
2228
2407
|
return parts.join(" | ");
|
|
2229
2408
|
}
|
|
2409
|
+
function cvssVersionToSchema(v) {
|
|
2410
|
+
switch (v) {
|
|
2411
|
+
case "2.0": return Version.The20;
|
|
2412
|
+
case "3.0": return Version.The30;
|
|
2413
|
+
case "4.0": return Version.The40;
|
|
2414
|
+
default: return Version.The31;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
function cvssBandSeverity(score) {
|
|
2418
|
+
switch (cvssScoreToSeverity(score)) {
|
|
2419
|
+
case "critical": return CVSSSeverity.Critical;
|
|
2420
|
+
case "high": return CVSSSeverity.High;
|
|
2421
|
+
case "medium": return CVSSSeverity.Medium;
|
|
2422
|
+
case "low": return CVSSSeverity.Low;
|
|
2423
|
+
default: return CVSSSeverity.None;
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
function buildCvssEntries$1(vuln) {
|
|
2427
|
+
if (!vuln.cvss || vuln.cvss.length === 0) return;
|
|
2428
|
+
const entries = [];
|
|
2429
|
+
for (const c of vuln.cvss) {
|
|
2430
|
+
const entry = { version: cvssVersionToSchema(c.version) };
|
|
2431
|
+
if (c.vector) entry.baseVector = c.vector;
|
|
2432
|
+
const score = c.metrics?.baseScore;
|
|
2433
|
+
if (typeof score === "number" && Number.isFinite(score)) {
|
|
2434
|
+
entry.baseScore = score;
|
|
2435
|
+
entry.baseSeverity = cvssBandSeverity(score);
|
|
2436
|
+
}
|
|
2437
|
+
if (vuln.id) entry.source = vuln.id;
|
|
2438
|
+
if (entry.baseVector === void 0 && entry.baseScore === void 0) continue;
|
|
2439
|
+
entries.push(entry);
|
|
2440
|
+
}
|
|
2441
|
+
return entries.length > 0 ? entries : void 0;
|
|
2442
|
+
}
|
|
2443
|
+
function mapGrypeTypeToEcosystem(grypeType) {
|
|
2444
|
+
switch ((grypeType ?? "").toLowerCase()) {
|
|
2445
|
+
case "rpm": return Ecosystem.RPM;
|
|
2446
|
+
case "deb": return Ecosystem.Deb;
|
|
2447
|
+
case "npm": return Ecosystem.Npm;
|
|
2448
|
+
case "python": return Ecosystem.Pypi;
|
|
2449
|
+
case "gem": return Ecosystem.Gem;
|
|
2450
|
+
case "go-module": return Ecosystem.Go;
|
|
2451
|
+
case "java-archive":
|
|
2452
|
+
case "jenkins-plugin": return Ecosystem.Maven;
|
|
2453
|
+
case "dotnet": return Ecosystem.Nuget;
|
|
2454
|
+
case "rust-crate": return Ecosystem.Cargo;
|
|
2455
|
+
default: return Ecosystem.Generic;
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
function buildAffectedPackages(match) {
|
|
2459
|
+
const artifact = match.artifact;
|
|
2460
|
+
const pkg = {
|
|
2461
|
+
name: artifact.name,
|
|
2462
|
+
version: artifact.version,
|
|
2463
|
+
ecosystem: mapGrypeTypeToEcosystem(artifact.type)
|
|
2464
|
+
};
|
|
2465
|
+
if (artifact.cpes && artifact.cpes.length > 0 && artifact.cpes[0]) pkg.cpe = artifact.cpes[0];
|
|
2466
|
+
if (artifact.purl) pkg.purl = artifact.purl;
|
|
2467
|
+
const fix = match.vulnerability.fix;
|
|
2468
|
+
if (fix && fix.state === "fixed" && fix.versions && fix.versions.length > 0 && fix.versions[0]) pkg.fixedInVersion = fix.versions[0];
|
|
2469
|
+
return [pkg];
|
|
2470
|
+
}
|
|
2471
|
+
const CWE_ID_PATTERN = /^CWE-[1-9]\d*$/;
|
|
2472
|
+
function extractCwe(raw) {
|
|
2473
|
+
if (!raw || raw.length === 0) return void 0;
|
|
2474
|
+
const out = raw.filter((c) => CWE_ID_PATTERN.test(c));
|
|
2475
|
+
return out.length === 0 ? void 0 : out;
|
|
2476
|
+
}
|
|
2477
|
+
function buildEpss$1(entries) {
|
|
2478
|
+
if (!entries || entries.length === 0) return void 0;
|
|
2479
|
+
const e = entries[0];
|
|
2480
|
+
if (!e.date) return void 0;
|
|
2481
|
+
return {
|
|
2482
|
+
score: e.epss ?? 0,
|
|
2483
|
+
percentile: e.percentile ?? 0,
|
|
2484
|
+
date: e.date
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
function buildKev(k) {
|
|
2488
|
+
if (!k) return void 0;
|
|
2489
|
+
const out = { inKev: Boolean(k.inKev) };
|
|
2490
|
+
if (k.dateAdded) out.dateAdded = k.dateAdded;
|
|
2491
|
+
if (k.dueDate) out.dueDate = k.dueDate;
|
|
2492
|
+
if (k.notes) out.notes = k.notes;
|
|
2493
|
+
return out;
|
|
2494
|
+
}
|
|
2230
2495
|
function convertMatchToRequirement(match, isIgnored) {
|
|
2231
2496
|
const vuln = match.vulnerability;
|
|
2232
2497
|
const cveId = vuln.id;
|
|
@@ -2277,6 +2542,15 @@ function convertMatchToRequirement(match, isIgnored) {
|
|
|
2277
2542
|
};
|
|
2278
2543
|
const controlType = deriveControlTypeFromTags(DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
|
|
2279
2544
|
if (controlType !== void 0) requirement.controlType = controlType;
|
|
2545
|
+
const cvss = buildCvssEntries$1(vuln);
|
|
2546
|
+
if (cvss) requirement.cvss = cvss;
|
|
2547
|
+
requirement.affectedPackages = buildAffectedPackages(match);
|
|
2548
|
+
const cwe = extractCwe(vuln.cwe);
|
|
2549
|
+
if (cwe) requirement.cwe = cwe;
|
|
2550
|
+
const epss = buildEpss$1(vuln.epss);
|
|
2551
|
+
if (epss) requirement.epss = epss;
|
|
2552
|
+
const kev = buildKev(vuln.kev);
|
|
2553
|
+
if (kev) requirement.kev = kev;
|
|
2280
2554
|
return requirement;
|
|
2281
2555
|
}
|
|
2282
2556
|
async function convertGrypeToHdf(input) {
|
|
@@ -2297,6 +2571,7 @@ async function convertGrypeToHdf(input) {
|
|
|
2297
2571
|
for (const match of limitedIgnored) requirements.push(convertMatchToRequirement(match, true));
|
|
2298
2572
|
}
|
|
2299
2573
|
const targetName = grypeData.source?.target?.userInput || "Grype Scan";
|
|
2574
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("grype-no-findings", `Grype scanned ${targetName} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
|
|
2300
2575
|
const baseline = createMinimalBaseline(targetName, requirements, { resultsChecksum });
|
|
2301
2576
|
return buildHdfResults({
|
|
2302
2577
|
generatorName: grypeData.descriptor?.name || "grype",
|
|
@@ -2305,14 +2580,16 @@ async function convertGrypeToHdf(input) {
|
|
|
2305
2580
|
toolVersion: grypeData.descriptor?.version,
|
|
2306
2581
|
baselines: [baseline],
|
|
2307
2582
|
components: [{
|
|
2308
|
-
type:
|
|
2583
|
+
type: TargetType.Artifact,
|
|
2309
2584
|
name: targetName
|
|
2310
2585
|
}],
|
|
2311
|
-
timestamp: grypeData.descriptor?.timestamp ?
|
|
2586
|
+
timestamp: (grypeData.descriptor?.timestamp ? parseTimestamp(grypeData.descriptor.timestamp) : null) ?? /* @__PURE__ */ new Date()
|
|
2312
2587
|
});
|
|
2313
2588
|
}
|
|
2314
2589
|
//#endregion
|
|
2315
2590
|
//#region converters/nessus-to-hdf/typescript/converter.ts
|
|
2591
|
+
const CVE_SOURCE_RE = /^CVE-\d{4}-\d{4,}$/;
|
|
2592
|
+
const CWE_PATTERN = /CWE[- ]?(\d+)/gi;
|
|
2316
2593
|
const converterVersion = "1.0.0";
|
|
2317
2594
|
const IMPACT_MAPPING$7 = {
|
|
2318
2595
|
"4": .9,
|
|
@@ -2340,7 +2617,9 @@ async function convertNessusToHdf(nessusXml) {
|
|
|
2340
2617
|
"preference",
|
|
2341
2618
|
"tag",
|
|
2342
2619
|
"ReportItem",
|
|
2343
|
-
"ReportHost"
|
|
2620
|
+
"ReportHost",
|
|
2621
|
+
"cwe",
|
|
2622
|
+
"cve"
|
|
2344
2623
|
]);
|
|
2345
2624
|
const policyName = parsed.NessusClientData_v2.Policy.policyName;
|
|
2346
2625
|
const version = extractVersion(parsed);
|
|
@@ -2362,7 +2641,7 @@ async function convertNessusToHdf(nessusXml) {
|
|
|
2362
2641
|
components,
|
|
2363
2642
|
statistics: { duration },
|
|
2364
2643
|
generator: {
|
|
2365
|
-
name: "hdf
|
|
2644
|
+
name: "nessus-to-hdf",
|
|
2366
2645
|
version: converterVersion
|
|
2367
2646
|
},
|
|
2368
2647
|
tool: { name: "Nessus" },
|
|
@@ -2387,8 +2666,8 @@ function calculateTiming(hosts) {
|
|
|
2387
2666
|
const lastHost = hosts[hosts.length - 1];
|
|
2388
2667
|
const startTimeStr = getHostPropertyValue(firstHost, "HOST_START");
|
|
2389
2668
|
const endTimeStr = getHostPropertyValue(lastHost, "HOST_END") || getHostPropertyValue(lastHost, "HOST_START");
|
|
2390
|
-
const startTime = startTimeStr ? new Date(
|
|
2391
|
-
const endTime = endTimeStr ?
|
|
2669
|
+
const startTime = startTimeStr ? parseTimestamp(startTimeStr) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date();
|
|
2670
|
+
const endTime = endTimeStr ? parseTimestamp(endTimeStr) ?? startTime : startTime;
|
|
2392
2671
|
return {
|
|
2393
2672
|
startTime,
|
|
2394
2673
|
endTime,
|
|
@@ -2409,6 +2688,12 @@ function convertReportHostToBaseline(host, policyName, version, resultsChecksum)
|
|
|
2409
2688
|
if (truncatedItems) console.warn(`WARNING: Input truncated at ${limitedItems.length} ReportItem items (original: ${items.length})`);
|
|
2410
2689
|
requirements = limitedItems.map((item) => convertReportItemToRequirement(item, host));
|
|
2411
2690
|
} else requirements = [];
|
|
2691
|
+
if (requirements.length === 0) {
|
|
2692
|
+
const target = host.name || getHostPropertyValue(host, "host-ip") || "host";
|
|
2693
|
+
const startTimeStr = getHostPropertyValue(host, "HOST_START");
|
|
2694
|
+
const startTime = startTimeStr ? parseTimestamp(startTimeStr) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date();
|
|
2695
|
+
requirements = [buildNoFindingsRequirement("nessus-no-findings", `Nessus scanned ${target} and reported zero findings.`, startTime)];
|
|
2696
|
+
}
|
|
2412
2697
|
return createMinimalBaseline(`Nessus ${policyName}`, requirements, {
|
|
2413
2698
|
title: `Nessus ${policyName}`,
|
|
2414
2699
|
version,
|
|
@@ -2442,8 +2727,141 @@ function convertReportItemToRequirement(item, host) {
|
|
|
2442
2727
|
};
|
|
2443
2728
|
if (controlType !== void 0) req.controlType = controlType;
|
|
2444
2729
|
if (verificationMethod !== void 0) req.verificationMethod = verificationMethod;
|
|
2730
|
+
if (!isCompliance) {
|
|
2731
|
+
const cvssEntries = buildCvssEntries(item);
|
|
2732
|
+
if (cvssEntries.length > 0) req.cvss = cvssEntries;
|
|
2733
|
+
const cweIDs = buildCweIDs(item);
|
|
2734
|
+
if (cweIDs.length > 0) req.cwe = cweIDs;
|
|
2735
|
+
const epss = buildEpss(item, host);
|
|
2736
|
+
if (epss !== void 0) req.epss = epss;
|
|
2737
|
+
}
|
|
2445
2738
|
return req;
|
|
2446
2739
|
}
|
|
2740
|
+
/**
|
|
2741
|
+
* Build a structured Cvss entry for a CVE finding. Returns an array because
|
|
2742
|
+
* the schema models cvss as a multi-entry array (Nessus emits one entry per
|
|
2743
|
+
* item; multi-vendor convergence may yield more).
|
|
2744
|
+
*/
|
|
2745
|
+
function buildCvssEntries(item) {
|
|
2746
|
+
const source = (item.cvss_score_source || "").trim();
|
|
2747
|
+
if (!source || !CVE_SOURCE_RE.test(source)) return [];
|
|
2748
|
+
const hasV3 = !!(item.cvss3_vector || item.cvss3_base_score);
|
|
2749
|
+
const hasV2 = !!(item.cvss_vector || item.cvss_base_score);
|
|
2750
|
+
if (!hasV3 && !hasV2) return [];
|
|
2751
|
+
let version;
|
|
2752
|
+
let baseVector;
|
|
2753
|
+
let baseScore;
|
|
2754
|
+
let threatVector;
|
|
2755
|
+
let threatScore;
|
|
2756
|
+
if (hasV3) {
|
|
2757
|
+
version = detectV3Version(item.cvss3_vector ?? "");
|
|
2758
|
+
baseVector = item.cvss3_vector || void 0;
|
|
2759
|
+
baseScore = parseFloatOrUndef(item.cvss3_base_score);
|
|
2760
|
+
threatVector = stripVersionPrefix(item.cvss3_temporal_vector);
|
|
2761
|
+
threatScore = item.cvss3_temporal_score ? parseFloatSafe(item.cvss3_temporal_score) : void 0;
|
|
2762
|
+
} else {
|
|
2763
|
+
version = Version.The20;
|
|
2764
|
+
baseVector = stripV2Prefix(item.cvss_vector ?? "") || void 0;
|
|
2765
|
+
baseScore = parseFloatOrUndef(item.cvss_base_score);
|
|
2766
|
+
threatVector = stripV2Prefix(item.cvss_temporal_vector ?? "") || void 0;
|
|
2767
|
+
threatScore = item.cvss_temporal_score ? parseFloatSafe(item.cvss_temporal_score) : void 0;
|
|
2768
|
+
}
|
|
2769
|
+
const entry = {
|
|
2770
|
+
version,
|
|
2771
|
+
source
|
|
2772
|
+
};
|
|
2773
|
+
if (baseVector) entry.baseVector = baseVector;
|
|
2774
|
+
if (baseScore !== void 0) {
|
|
2775
|
+
entry.baseScore = baseScore;
|
|
2776
|
+
const baseSeverity = mapCvssSeverity(baseScore);
|
|
2777
|
+
if (baseSeverity !== void 0) entry.baseSeverity = baseSeverity;
|
|
2778
|
+
}
|
|
2779
|
+
if (threatVector !== void 0 && threatVector !== "") entry.threatVector = threatVector;
|
|
2780
|
+
if (threatScore !== void 0) {
|
|
2781
|
+
entry.threatScore = threatScore;
|
|
2782
|
+
entry.computedScore = threatScore;
|
|
2783
|
+
const computedSeverity = mapCvssSeverity(threatScore);
|
|
2784
|
+
if (computedSeverity !== void 0) entry.computedSeverity = computedSeverity;
|
|
2785
|
+
}
|
|
2786
|
+
return [entry];
|
|
2787
|
+
}
|
|
2788
|
+
function detectV3Version(vector) {
|
|
2789
|
+
if (vector.startsWith("CVSS:3.1/")) return Version.The31;
|
|
2790
|
+
if (vector.startsWith("CVSS:3.0/")) return Version.The30;
|
|
2791
|
+
return Version.The30;
|
|
2792
|
+
}
|
|
2793
|
+
function stripVersionPrefix(vector) {
|
|
2794
|
+
if (!vector) return void 0;
|
|
2795
|
+
for (const prefix of [
|
|
2796
|
+
"CVSS:3.0/",
|
|
2797
|
+
"CVSS:3.1/",
|
|
2798
|
+
"CVSS:4.0/"
|
|
2799
|
+
]) if (vector.startsWith(prefix)) return vector.slice(prefix.length);
|
|
2800
|
+
return vector;
|
|
2801
|
+
}
|
|
2802
|
+
function stripV2Prefix(vector) {
|
|
2803
|
+
return vector.startsWith("CVSS2#") ? vector.slice(6) : vector;
|
|
2804
|
+
}
|
|
2805
|
+
function parseFloatSafe(s) {
|
|
2806
|
+
if (!s) return 0;
|
|
2807
|
+
const f = Number.parseFloat(s);
|
|
2808
|
+
return Number.isFinite(f) ? f : 0;
|
|
2809
|
+
}
|
|
2810
|
+
function parseFloatOrUndef(s) {
|
|
2811
|
+
if (s === void 0 || s === "") return void 0;
|
|
2812
|
+
const f = Number.parseFloat(s);
|
|
2813
|
+
return Number.isFinite(f) ? f : void 0;
|
|
2814
|
+
}
|
|
2815
|
+
function mapCvssSeverity(score) {
|
|
2816
|
+
switch (cvssScoreToSeverity(score)) {
|
|
2817
|
+
case "critical": return CVSSSeverity.Critical;
|
|
2818
|
+
case "high": return CVSSSeverity.High;
|
|
2819
|
+
case "medium": return CVSSSeverity.Medium;
|
|
2820
|
+
case "low": return CVSSSeverity.Low;
|
|
2821
|
+
case "none": return CVSSSeverity.None;
|
|
2822
|
+
default: return;
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
/**
|
|
2826
|
+
* Extract CWE IDs from a ReportItem's <cwe> elements. Nessus emits bare
|
|
2827
|
+
* numeric IDs (e.g. <cwe>200</cwe>); occasionally pipe-separated or prefixed
|
|
2828
|
+
* forms appear. Output is "CWE-N" form per schema convention.
|
|
2829
|
+
*/
|
|
2830
|
+
function buildCweIDs(item) {
|
|
2831
|
+
if (!item.cwe) return [];
|
|
2832
|
+
const raws = Array.isArray(item.cwe) ? item.cwe : [item.cwe];
|
|
2833
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2834
|
+
for (const raw of raws) {
|
|
2835
|
+
const text = String(raw);
|
|
2836
|
+
for (const m of text.matchAll(CWE_PATTERN)) if (m[1]) seen.add(m[1]);
|
|
2837
|
+
for (const tok of text.split(/[^0-9]+/)) if (tok !== "") seen.add(tok);
|
|
2838
|
+
}
|
|
2839
|
+
if (seen.size === 0) return [];
|
|
2840
|
+
return [...seen].sort().map((id) => `CWE-${id}`);
|
|
2841
|
+
}
|
|
2842
|
+
/**
|
|
2843
|
+
* Build a structured Epss entry when the ReportItem includes EPSS data.
|
|
2844
|
+
* The date is derived from the host's HOST_START in YYYY-MM-DD form.
|
|
2845
|
+
*/
|
|
2846
|
+
function buildEpss(item, host) {
|
|
2847
|
+
const hasScore = item.epss_score !== void 0 && item.epss_score !== "";
|
|
2848
|
+
const hasPct = item.epss_percentile !== void 0 && item.epss_percentile !== "";
|
|
2849
|
+
if (!hasScore && !hasPct) return void 0;
|
|
2850
|
+
const date = epssDate(host);
|
|
2851
|
+
if (date === void 0) return void 0;
|
|
2852
|
+
return {
|
|
2853
|
+
date,
|
|
2854
|
+
score: hasScore ? parseFloatSafe(item.epss_score) : 0,
|
|
2855
|
+
percentile: hasPct ? parseFloatSafe(item.epss_percentile) : 0
|
|
2856
|
+
};
|
|
2857
|
+
}
|
|
2858
|
+
function epssDate(host) {
|
|
2859
|
+
const hs = getHostPropertyValue(host, "HOST_START");
|
|
2860
|
+
if (hs) {
|
|
2861
|
+
const d = parseTimestamp(hs);
|
|
2862
|
+
if (d) return d.toISOString().slice(0, 10);
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2447
2865
|
function buildDescriptions(item, isCompliance) {
|
|
2448
2866
|
const descriptions = [];
|
|
2449
2867
|
if (isCompliance && item["compliance-info"]) descriptions.push({
|
|
@@ -2504,7 +2922,7 @@ function buildTags$2(item, isCompliance) {
|
|
|
2504
2922
|
}
|
|
2505
2923
|
function buildRefs(item) {
|
|
2506
2924
|
const refs = [];
|
|
2507
|
-
if (item.see_also) refs.push({ url
|
|
2925
|
+
if (item.see_also) for (const url of item.see_also.split(/\s+/).filter(Boolean)) refs.push({ url });
|
|
2508
2926
|
return refs.length > 0 ? refs : void 0;
|
|
2509
2927
|
}
|
|
2510
2928
|
function buildResult$1(item, host, isCompliance) {
|
|
@@ -2512,7 +2930,7 @@ function buildResult$1(item, host, isCompliance) {
|
|
|
2512
2930
|
const codeDesc = getCodeDesc(item);
|
|
2513
2931
|
const message = item.plugin_output || item["compliance-actual-value"];
|
|
2514
2932
|
const startTimeStr = getHostPropertyValue(host, "HOST_START");
|
|
2515
|
-
const startTime = startTimeStr ? new Date(
|
|
2933
|
+
const startTime = startTimeStr ? parseTimestamp(startTimeStr) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date();
|
|
2516
2934
|
return {
|
|
2517
2935
|
status,
|
|
2518
2936
|
codeDesc,
|
|
@@ -2546,7 +2964,7 @@ function convertReportHostToTarget(host) {
|
|
|
2546
2964
|
});
|
|
2547
2965
|
const target = {
|
|
2548
2966
|
name: hostName,
|
|
2549
|
-
type:
|
|
2967
|
+
type: TargetType.Host
|
|
2550
2968
|
};
|
|
2551
2969
|
if (isFQDN(hostName)) target.fqdn = hostName;
|
|
2552
2970
|
const hostIp = hostProps["host-ip"];
|
|
@@ -2614,18 +3032,40 @@ async function convertSonarqubeToHdf(input) {
|
|
|
2614
3032
|
const baseline = convertProjectToBaseline(projectKey, issues, componentMap, ruleMap, resultsChecksum);
|
|
2615
3033
|
baselines.push(baseline);
|
|
2616
3034
|
}
|
|
3035
|
+
let components = Array.from(issuesByProject.keys()).map((projectKey) => ({
|
|
3036
|
+
type: TargetType.Application,
|
|
3037
|
+
name: projectKey
|
|
3038
|
+
}));
|
|
3039
|
+
if (baselines.length === 0) {
|
|
3040
|
+
const targetName = deriveEmptyScanTarget(sonarData.components);
|
|
3041
|
+
baselines.push({
|
|
3042
|
+
name: targetName,
|
|
3043
|
+
title: `SonarQube Analysis for ${targetName}`,
|
|
3044
|
+
requirements: [buildNoFindingsRequirement("sonarqube-no-findings", `SonarQube scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date())],
|
|
3045
|
+
resultsChecksum
|
|
3046
|
+
});
|
|
3047
|
+
components = [{
|
|
3048
|
+
type: TargetType.Application,
|
|
3049
|
+
name: targetName
|
|
3050
|
+
}];
|
|
3051
|
+
}
|
|
2617
3052
|
return buildHdfResults({
|
|
2618
3053
|
generatorName: "sonarqube-to-hdf",
|
|
2619
3054
|
converterVersion: "1.0.0",
|
|
2620
3055
|
toolName: "SonarQube",
|
|
2621
3056
|
baselines,
|
|
2622
|
-
components
|
|
2623
|
-
type: Copyright.Application,
|
|
2624
|
-
name: projectKey
|
|
2625
|
-
})),
|
|
3057
|
+
components,
|
|
2626
3058
|
timestamp: /* @__PURE__ */ new Date()
|
|
2627
3059
|
});
|
|
2628
3060
|
}
|
|
3061
|
+
function deriveEmptyScanTarget(components) {
|
|
3062
|
+
if (components) {
|
|
3063
|
+
const project = components.find((c) => c.qualifier === "TRK");
|
|
3064
|
+
if (project) return project.key;
|
|
3065
|
+
if (components.length > 0) return components[0].key;
|
|
3066
|
+
}
|
|
3067
|
+
return "the SonarQube project";
|
|
3068
|
+
}
|
|
2629
3069
|
function convertProjectToBaseline(projectKey, issues, componentMap, ruleMap, resultsChecksum) {
|
|
2630
3070
|
const issuesByRule = /* @__PURE__ */ new Map();
|
|
2631
3071
|
for (const issue of issues) {
|
|
@@ -2725,7 +3165,7 @@ function createResultFromIssue(issue, componentMap) {
|
|
|
2725
3165
|
const codeDesc = `${component?.path || component?.longName || issue.component}${issue.line ? ` LINE : ${issue.line}` : ""}`;
|
|
2726
3166
|
return createResult(status, issue.message, {
|
|
2727
3167
|
codeDesc,
|
|
2728
|
-
startTime:
|
|
3168
|
+
startTime: parseTimestamp(issue.creationDate) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
|
|
2729
3169
|
});
|
|
2730
3170
|
}
|
|
2731
3171
|
function extractSourceLocation(issue, componentMap) {
|
|
@@ -2762,6 +3202,28 @@ function buildNistTags$3(sourceIdentifier, ruleName) {
|
|
|
2762
3202
|
if (byName) return [byName];
|
|
2763
3203
|
return [];
|
|
2764
3204
|
}
|
|
3205
|
+
/**
|
|
3206
|
+
* Flag rules whose NIST mappings exist at a revision other than the one
|
|
3207
|
+
* currently selected — they emit no NIST tags here even though a mapping
|
|
3208
|
+
* exists elsewhere, a likely sign of a wrong revision selection. Rules
|
|
3209
|
+
* unmapped at every revision are not flagged. Throws in strict mode; otherwise
|
|
3210
|
+
* logs a single aggregated warning.
|
|
3211
|
+
*/
|
|
3212
|
+
function checkRevisionAlignment(rules) {
|
|
3213
|
+
const rev = getCurrentNistRevision();
|
|
3214
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3215
|
+
const lines = [];
|
|
3216
|
+
for (const rule of rules) {
|
|
3217
|
+
const covered = awsConfigMappedRevisions(rule.Source.SourceIdentifier, rule.ConfigRuleName);
|
|
3218
|
+
if (covered.length === 0 || covered.includes(rev) || seen.has(rule.ConfigRuleName)) continue;
|
|
3219
|
+
seen.add(rule.ConfigRuleName);
|
|
3220
|
+
lines.push(` - ${rule.ConfigRuleName} (mapped at Rev ${covered.join(", ")})`);
|
|
3221
|
+
}
|
|
3222
|
+
if (lines.length === 0) return;
|
|
3223
|
+
const detail = `${lines.length} AWS Config rule(s) have NIST 800-53 mappings at a different revision than the requested Rev ${rev}; their NIST tags were omitted:\n${lines.join("\n")}`;
|
|
3224
|
+
if (isNistStrict()) throw new Error(`aws-config: ${detail}\nre-run with a matching revision, or disable strict mode to convert with the gaps`);
|
|
3225
|
+
console.warn(`WARNING: ${detail}`);
|
|
3226
|
+
}
|
|
2765
3227
|
function buildCheckText(rule) {
|
|
2766
3228
|
const parts = [`ARN: ${rule.ConfigRuleArn || "N/A"}`, `Source Identifier: ${rule.Source.SourceIdentifier || "N/A"}`];
|
|
2767
3229
|
if (rule.InputParameters && rule.InputParameters !== "{}") {
|
|
@@ -2782,12 +3244,26 @@ function buildResult(r) {
|
|
|
2782
3244
|
const status = mapComplianceStatus(r.ComplianceType);
|
|
2783
3245
|
const codeDesc = buildCodeDesc$7(q);
|
|
2784
3246
|
const message = buildResultMessage(codeDesc, r.Annotation, status);
|
|
2785
|
-
const startTime = r.ConfigRuleInvokedTime ?
|
|
3247
|
+
const startTime = (r.ConfigRuleInvokedTime ? parseTimestamp(r.ConfigRuleInvokedTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
|
|
2786
3248
|
return createResult(status, message ?? codeDesc, {
|
|
2787
3249
|
codeDesc,
|
|
2788
3250
|
...startTime ? { startTime } : {}
|
|
2789
3251
|
});
|
|
2790
3252
|
}
|
|
3253
|
+
/**
|
|
3254
|
+
* Synthesizes a single HDF result for a Config rule whose live evaluation
|
|
3255
|
+
* returned zero in-scope resources. The HDF schema requires `results` to have
|
|
3256
|
+
* minItems >= 1; this honestly signals to auditors that the rule's check ran
|
|
3257
|
+
* but had no scope in this account/region rather than vacuously claiming
|
|
3258
|
+
* "passed". See issue #80 bug 2.
|
|
3259
|
+
*/
|
|
3260
|
+
function buildNotApplicableResult(rule) {
|
|
3261
|
+
const codeDesc = `AWS Config rule ${rule.ConfigRuleName} evaluated zero in-scope resources in this account/region.`;
|
|
3262
|
+
return createResult(ResultStatus.NotApplicable, codeDesc, {
|
|
3263
|
+
codeDesc,
|
|
3264
|
+
startTime: /* @__PURE__ */ new Date()
|
|
3265
|
+
});
|
|
3266
|
+
}
|
|
2791
3267
|
function buildRequirement$15(rule) {
|
|
2792
3268
|
const nist = buildNistTags$3(rule.Source.SourceIdentifier, rule.ConfigRuleName);
|
|
2793
3269
|
const tags = nist.length > 0 ? { nist } : {};
|
|
@@ -2799,7 +3275,7 @@ function buildRequirement$15(rule) {
|
|
|
2799
3275
|
data: buildCheckText(rule)
|
|
2800
3276
|
}];
|
|
2801
3277
|
const title = `${getAccountId(rule.ConfigRuleArn)} - ${rule.ConfigRuleName}`;
|
|
2802
|
-
const results = rule.EvaluationResults.map(buildResult);
|
|
3278
|
+
const results = rule.EvaluationResults.length > 0 ? rule.EvaluationResults.map(buildResult) : [buildNotApplicableResult(rule)];
|
|
2803
3279
|
const req = createRequirement(rule.ConfigRuleId, title, descriptions, .5, results, {
|
|
2804
3280
|
tags,
|
|
2805
3281
|
sourceLocation: {
|
|
@@ -2828,6 +3304,7 @@ async function convertAwsConfigToHdf(input) {
|
|
|
2828
3304
|
const { items: limitedRules, truncated: truncatedRules } = limitArray(data.ConfigRules);
|
|
2829
3305
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
2830
3306
|
if (truncatedRules) console.warn(`WARNING: Input truncated at ${limitedRules.length} ConfigRule items (original: ${data.ConfigRules.length})`);
|
|
3307
|
+
checkRevisionAlignment(limitedRules);
|
|
2831
3308
|
const baseline = {
|
|
2832
3309
|
...createMinimalBaseline("AWS Config", limitedRules.map(buildRequirement$15), { resultsChecksum }),
|
|
2833
3310
|
title: "AWS Config Compliance Results",
|
|
@@ -2843,7 +3320,7 @@ async function convertAwsConfigToHdf(input) {
|
|
|
2843
3320
|
toolName: "AWS Config",
|
|
2844
3321
|
baselines: [baseline],
|
|
2845
3322
|
components: [{
|
|
2846
|
-
type:
|
|
3323
|
+
type: TargetType.CloudAccount,
|
|
2847
3324
|
name: `AWS Account ${accountId}`,
|
|
2848
3325
|
labels: {
|
|
2849
3326
|
account: accountId,
|
|
@@ -2877,17 +3354,20 @@ function getImpact$8(severity) {
|
|
|
2877
3354
|
/**
|
|
2878
3355
|
* Converts a single CheckovCheck to an HDF RequirementResult.
|
|
2879
3356
|
*/
|
|
2880
|
-
function checkToResult(check) {
|
|
3357
|
+
function checkToResult(check, scanTime) {
|
|
2881
3358
|
const status = mapStatus$2(check.check_result.result);
|
|
2882
3359
|
const codeDesc = `Resource: ${check.resource}\nFile: ${check.file_path} (lines ${JSON.stringify(check.file_line_range)})`;
|
|
2883
3360
|
let message;
|
|
2884
3361
|
if (status === ResultStatus.NotReviewed && check.check_result.suppress_comment) message = check.check_result.suppress_comment;
|
|
2885
|
-
return createResult(status, message ?? "", {
|
|
3362
|
+
return createResult(status, message ?? "", {
|
|
3363
|
+
codeDesc,
|
|
3364
|
+
startTime: scanTime
|
|
3365
|
+
});
|
|
2886
3366
|
}
|
|
2887
3367
|
/**
|
|
2888
3368
|
* Converts a group of checks sharing a check_id into one EvaluatedRequirement.
|
|
2889
3369
|
*/
|
|
2890
|
-
function buildRequirement$14(checkId, checks) {
|
|
3370
|
+
function buildRequirement$14(checkId, checks, scanTime) {
|
|
2891
3371
|
const rep = checks[0];
|
|
2892
3372
|
const impact = getImpact$8(rep.severity);
|
|
2893
3373
|
const tags = { nist: [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS] };
|
|
@@ -2899,7 +3379,7 @@ function buildRequirement$14(checkId, checks) {
|
|
|
2899
3379
|
label: "check",
|
|
2900
3380
|
data: rep.guideline
|
|
2901
3381
|
});
|
|
2902
|
-
const results = checks.map(checkToResult);
|
|
3382
|
+
const results = checks.map((check) => checkToResult(check, scanTime));
|
|
2903
3383
|
const req = createRequirement(checkId, rep.check_name, descriptions, impact, results, { tags });
|
|
2904
3384
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
2905
3385
|
const controlType = deriveControlTypeFromTags([...DEFAULT_STATIC_ANALYSIS_NIST_TAGS]);
|
|
@@ -2930,6 +3410,7 @@ async function convertCheckovToHdf(input) {
|
|
|
2930
3410
|
const detected = detectConverter(input);
|
|
2931
3411
|
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
|
|
2932
3412
|
const resultsChecksum = await inputChecksum(input);
|
|
3413
|
+
const scanTime = /* @__PURE__ */ new Date();
|
|
2933
3414
|
const reports = parseInput(input);
|
|
2934
3415
|
const allChecks = [];
|
|
2935
3416
|
const checkTypes = [];
|
|
@@ -2951,9 +3432,13 @@ async function convertCheckovToHdf(input) {
|
|
|
2951
3432
|
else groups.set(check.check_id, [check]);
|
|
2952
3433
|
}
|
|
2953
3434
|
const requirements = [];
|
|
2954
|
-
for (const [checkId, checks] of groups) requirements.push(buildRequirement$14(checkId, checks));
|
|
2955
|
-
const baseline = createMinimalBaseline("Checkov Scan", requirements, { resultsChecksum });
|
|
3435
|
+
for (const [checkId, checks] of groups) requirements.push(buildRequirement$14(checkId, checks, scanTime));
|
|
2956
3436
|
const format = checkTypes.join(", ");
|
|
3437
|
+
if (requirements.length === 0) {
|
|
3438
|
+
const target = format || "input";
|
|
3439
|
+
requirements.push(buildNoFindingsRequirement("checkov-no-findings", `Checkov scanned ${target} and reported zero findings.`, scanTime));
|
|
3440
|
+
}
|
|
3441
|
+
const baseline = createMinimalBaseline("Checkov Scan", requirements, { resultsChecksum });
|
|
2957
3442
|
return buildHdfResults({
|
|
2958
3443
|
generatorName: "checkov-to-hdf",
|
|
2959
3444
|
converterVersion: "1.0.0",
|
|
@@ -2961,7 +3446,7 @@ async function convertCheckovToHdf(input) {
|
|
|
2961
3446
|
toolVersion: version,
|
|
2962
3447
|
toolFormat: format,
|
|
2963
3448
|
baselines: [baseline],
|
|
2964
|
-
timestamp:
|
|
3449
|
+
timestamp: scanTime
|
|
2965
3450
|
});
|
|
2966
3451
|
}
|
|
2967
3452
|
//#endregion
|
|
@@ -2992,19 +3477,22 @@ function formatSkipMessage(suppressions) {
|
|
|
2992
3477
|
/**
|
|
2993
3478
|
* Converts a single GosecIssue to an HDF RequirementResult.
|
|
2994
3479
|
*/
|
|
2995
|
-
function issueToResult(issue) {
|
|
3480
|
+
function issueToResult(issue, scanTime) {
|
|
2996
3481
|
const suppressed = isSuppressed(issue);
|
|
2997
3482
|
const status = suppressed ? ResultStatus.NotReviewed : ResultStatus.Failed;
|
|
2998
3483
|
let message;
|
|
2999
3484
|
if (suppressed) message = formatSkipMessage(issue.suppressions) ?? "No justification provided";
|
|
3000
3485
|
else message = `${issue.confidence} confidence of rule violation at:\n${issue.code}`;
|
|
3001
3486
|
const codeDesc = `Rule ${issue.rule_id} violation detected at:\nFile: ${issue.file}\nLine: ${issue.line}\nColumn: ${issue.column}`;
|
|
3002
|
-
return createResult(status, message, {
|
|
3487
|
+
return createResult(status, message, {
|
|
3488
|
+
codeDesc,
|
|
3489
|
+
startTime: scanTime
|
|
3490
|
+
});
|
|
3003
3491
|
}
|
|
3004
3492
|
/**
|
|
3005
3493
|
* Converts a group of issues sharing a rule_id into one EvaluatedRequirement.
|
|
3006
3494
|
*/
|
|
3007
|
-
function buildRequirement$13(ruleId, issues) {
|
|
3495
|
+
function buildRequirement$13(ruleId, issues, scanTime) {
|
|
3008
3496
|
const rep = issues[0];
|
|
3009
3497
|
const impact = IMPACT_MAPPING$6[rep.severity.toUpperCase()] ?? .5;
|
|
3010
3498
|
const nist = mapCWEToNIST([rep.cwe.id], DEFAULT_REMEDIATION_NIST_TAGS$1);
|
|
@@ -3015,7 +3503,7 @@ function buildRequirement$13(ruleId, issues) {
|
|
|
3015
3503
|
url: rep.cwe.url
|
|
3016
3504
|
}
|
|
3017
3505
|
};
|
|
3018
|
-
const results = issues.map(issueToResult);
|
|
3506
|
+
const results = issues.map((issue) => issueToResult(issue, scanTime));
|
|
3019
3507
|
const descriptions = [{
|
|
3020
3508
|
label: "default",
|
|
3021
3509
|
data: rep.details
|
|
@@ -3055,8 +3543,10 @@ async function convertGosecToHdf(input) {
|
|
|
3055
3543
|
if (existing) existing.push(issue);
|
|
3056
3544
|
else groups.set(issue.rule_id, [issue]);
|
|
3057
3545
|
}
|
|
3546
|
+
const scanTime = /* @__PURE__ */ new Date();
|
|
3058
3547
|
const requirements = [];
|
|
3059
|
-
for (const [ruleId, issues] of groups) requirements.push(buildRequirement$13(ruleId, issues));
|
|
3548
|
+
for (const [ruleId, issues] of groups) requirements.push(buildRequirement$13(ruleId, issues, scanTime));
|
|
3549
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("gosec-no-findings", "gosec scanned Go codebase and reported zero findings.", scanTime));
|
|
3060
3550
|
const baseline = createMinimalBaseline("gosec Scan", requirements, { resultsChecksum });
|
|
3061
3551
|
return buildHdfResults({
|
|
3062
3552
|
generatorName: "gosec-to-hdf",
|
|
@@ -3064,7 +3554,7 @@ async function convertGosecToHdf(input) {
|
|
|
3064
3554
|
toolName: "gosec",
|
|
3065
3555
|
toolVersion: report.GosecVersion || void 0,
|
|
3066
3556
|
baselines: [baseline],
|
|
3067
|
-
timestamp:
|
|
3557
|
+
timestamp: scanTime
|
|
3068
3558
|
});
|
|
3069
3559
|
}
|
|
3070
3560
|
//#endregion
|
|
@@ -3137,6 +3627,7 @@ async function convertNiktoToHdf(input) {
|
|
|
3137
3627
|
if (niktoData.host) targetParts.push(`Host: ${niktoData.host}`);
|
|
3138
3628
|
if (niktoData.port) targetParts.push(`Port: ${niktoData.port}`);
|
|
3139
3629
|
const targetName = targetParts.length > 0 ? targetParts.join(" ") : "Nikto Scan";
|
|
3630
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("nikto-no-findings", `Nikto scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
3140
3631
|
return buildHdfResults({
|
|
3141
3632
|
generatorName: "nikto-to-hdf",
|
|
3142
3633
|
converterVersion: "unknown",
|
|
@@ -3148,7 +3639,7 @@ async function convertNiktoToHdf(input) {
|
|
|
3148
3639
|
summary: niktoData.banner || ""
|
|
3149
3640
|
})],
|
|
3150
3641
|
components: [{
|
|
3151
|
-
type:
|
|
3642
|
+
type: TargetType.Application,
|
|
3152
3643
|
name: targetName
|
|
3153
3644
|
}]
|
|
3154
3645
|
});
|
|
@@ -3203,6 +3694,15 @@ function selectSite(sites) {
|
|
|
3203
3694
|
}
|
|
3204
3695
|
return best;
|
|
3205
3696
|
}
|
|
3697
|
+
const ZAP_RFC1123_LIKE = /^[A-Za-z]{3}, \d{1,2} [A-Za-z]{3} \d{4} \d{2}:\d{2}:\d{2}$/;
|
|
3698
|
+
function parseZapTimestamp(s) {
|
|
3699
|
+
const trimmed = s.trim();
|
|
3700
|
+
if (ZAP_RFC1123_LIKE.test(trimmed)) {
|
|
3701
|
+
const d = /* @__PURE__ */ new Date(`${trimmed} GMT`);
|
|
3702
|
+
if (!isNaN(d.getTime())) return d;
|
|
3703
|
+
}
|
|
3704
|
+
return parseTimestamp(s) ?? void 0;
|
|
3705
|
+
}
|
|
3206
3706
|
async function convertZapToHdf(input) {
|
|
3207
3707
|
validateInputSize(input, "zap");
|
|
3208
3708
|
registerAllFingerprints();
|
|
@@ -3210,45 +3710,8 @@ async function convertZapToHdf(input) {
|
|
|
3210
3710
|
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
|
|
3211
3711
|
const resultsChecksum = await inputChecksum(input);
|
|
3212
3712
|
const zapData = parseJSON(input);
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
baselines: [createMinimalBaseline("OWASP ZAP Scan", [], {
|
|
3216
|
-
resultsChecksum,
|
|
3217
|
-
title: "OWASP ZAP Scan",
|
|
3218
|
-
summary: ""
|
|
3219
|
-
})],
|
|
3220
|
-
generator: {
|
|
3221
|
-
name: "zap-to-hdf",
|
|
3222
|
-
version: "unknown"
|
|
3223
|
-
},
|
|
3224
|
-
tool: {
|
|
3225
|
-
name: "OWASP ZAP",
|
|
3226
|
-
format: "JSON"
|
|
3227
|
-
}
|
|
3228
|
-
};
|
|
3229
|
-
return JSON.stringify(hdf, null, 2);
|
|
3230
|
-
}
|
|
3231
|
-
const site = selectSite(zapData.site);
|
|
3232
|
-
if (!site) {
|
|
3233
|
-
const hdf = {
|
|
3234
|
-
baselines: [createMinimalBaseline("OWASP ZAP Scan", [], {
|
|
3235
|
-
resultsChecksum,
|
|
3236
|
-
title: "OWASP ZAP Scan",
|
|
3237
|
-
summary: `ZAP Version ${zapData["@version"] ?? "unknown"}`
|
|
3238
|
-
})],
|
|
3239
|
-
generator: {
|
|
3240
|
-
name: "zap-to-hdf",
|
|
3241
|
-
version: "unknown"
|
|
3242
|
-
},
|
|
3243
|
-
tool: {
|
|
3244
|
-
name: "OWASP ZAP",
|
|
3245
|
-
format: "JSON"
|
|
3246
|
-
}
|
|
3247
|
-
};
|
|
3248
|
-
if (zapData["@generated"]) hdf.timestamp = new Date(zapData["@generated"]);
|
|
3249
|
-
return JSON.stringify(hdf, null, 2);
|
|
3250
|
-
}
|
|
3251
|
-
const alerts = site.alerts ?? [];
|
|
3713
|
+
const site = selectSite(Array.isArray(zapData.site) ? zapData.site : []);
|
|
3714
|
+
const alerts = site?.alerts ?? [];
|
|
3252
3715
|
const pluginIdCount = /* @__PURE__ */ new Map();
|
|
3253
3716
|
const { items: limitedAlerts, truncated } = limitArray(alerts);
|
|
3254
3717
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
@@ -3304,10 +3767,17 @@ async function convertZapToHdf(input) {
|
|
|
3304
3767
|
if (controlType !== void 0) req.controlType = controlType;
|
|
3305
3768
|
requirements.push(req);
|
|
3306
3769
|
}
|
|
3307
|
-
const targetName = site["@host"] ?? "Unknown Host";
|
|
3770
|
+
const targetName = site?.["@host"] ?? "Unknown Host";
|
|
3771
|
+
const siteName = site?.["@name"] ?? "";
|
|
3772
|
+
const baselineName = site && (site["@name"] || site["@host"]) ? `OWASP ZAP Scan of ${site["@name"] ?? targetName}` : "OWASP ZAP Scan";
|
|
3773
|
+
if (requirements.length === 0) {
|
|
3774
|
+
let target = siteName || targetName;
|
|
3775
|
+
if (!target || target === "Unknown Host") target = "the target site";
|
|
3776
|
+
requirements.push(buildNoFindingsRequirement("zap-no-findings", `OWASP ZAP scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
3777
|
+
}
|
|
3308
3778
|
const baseline = createMinimalBaseline("OWASP ZAP Scan", requirements, {
|
|
3309
3779
|
resultsChecksum,
|
|
3310
|
-
title:
|
|
3780
|
+
title: baselineName,
|
|
3311
3781
|
summary: `ZAP Version ${zapData["@version"] ?? "unknown"}`
|
|
3312
3782
|
});
|
|
3313
3783
|
const tool = {
|
|
@@ -3316,14 +3786,14 @@ async function convertZapToHdf(input) {
|
|
|
3316
3786
|
};
|
|
3317
3787
|
if (zapData["@version"]) tool.version = zapData["@version"];
|
|
3318
3788
|
const components = [];
|
|
3319
|
-
if (site["@name"]) components.push({
|
|
3789
|
+
if (site?.["@name"]) components.push({
|
|
3320
3790
|
name: targetName,
|
|
3321
|
-
type:
|
|
3791
|
+
type: TargetType.Application,
|
|
3322
3792
|
url: site["@name"]
|
|
3323
3793
|
});
|
|
3324
3794
|
else if (targetName !== "Unknown Host") components.push({
|
|
3325
3795
|
name: targetName,
|
|
3326
|
-
type:
|
|
3796
|
+
type: TargetType.Application
|
|
3327
3797
|
});
|
|
3328
3798
|
const hdf = {
|
|
3329
3799
|
baselines: [baseline],
|
|
@@ -3334,12 +3804,15 @@ async function convertZapToHdf(input) {
|
|
|
3334
3804
|
},
|
|
3335
3805
|
tool
|
|
3336
3806
|
};
|
|
3337
|
-
if (zapData["@generated"])
|
|
3338
|
-
|
|
3807
|
+
if (zapData["@generated"]) {
|
|
3808
|
+
const ts = parseZapTimestamp(zapData["@generated"]);
|
|
3809
|
+
if (ts) hdf.timestamp = ts;
|
|
3810
|
+
}
|
|
3811
|
+
return serializeHdf(hdf);
|
|
3339
3812
|
}
|
|
3340
3813
|
//#endregion
|
|
3341
3814
|
//#region converters/cyclonedx-to-hdf/typescript/converter.ts
|
|
3342
|
-
const CVSS_METHODS = new Set([
|
|
3815
|
+
const CVSS_METHODS = /* @__PURE__ */ new Set([
|
|
3343
3816
|
"CVSSv2",
|
|
3344
3817
|
"CVSSv3",
|
|
3345
3818
|
"CVSSv31",
|
|
@@ -3404,6 +3877,8 @@ async function convertCyclonedxToHdf(input) {
|
|
|
3404
3877
|
if ((!bom.components || bom.components.length === 0) && (!bom.vulnerabilities || bom.vulnerabilities.length === 0)) throw new Error("cyclonedx: input has neither components nor vulnerabilities");
|
|
3405
3878
|
if (!bom.vulnerabilities || bom.vulnerabilities.length === 0) throw new Error("cyclonedx: this file is an SBOM inventory with no vulnerabilities; to import SBOM data into a system document, use:\n hdf system create --from <sbom-file> --component-name <name>");
|
|
3406
3879
|
const resultsChecksum = await inputChecksum(input);
|
|
3880
|
+
const parsedTimestamp = bom.metadata?.timestamp ? parseTimestamp(bom.metadata.timestamp) ?? void 0 : void 0;
|
|
3881
|
+
const scanTime = parsedTimestamp && !isNaN(parsedTimestamp.getTime()) ? parsedTimestamp : /* @__PURE__ */ new Date();
|
|
3407
3882
|
const allComponents = flattenComponents(bom.components ?? []);
|
|
3408
3883
|
const componentLookup = /* @__PURE__ */ new Map();
|
|
3409
3884
|
for (const comp of allComponents) if (comp["bom-ref"]) componentLookup.set(comp["bom-ref"], comp);
|
|
@@ -3442,7 +3917,13 @@ async function convertCyclonedxToHdf(input) {
|
|
|
3442
3917
|
data: fixParts.join("\n\n")
|
|
3443
3918
|
});
|
|
3444
3919
|
const affects = vuln.affects ?? [];
|
|
3445
|
-
const results = affects.length > 0 ? affects.map((affect) => createResult(ResultStatus.Failed, void 0, {
|
|
3920
|
+
const results = affects.length > 0 ? affects.map((affect) => createResult(ResultStatus.Failed, void 0, {
|
|
3921
|
+
codeDesc: formatCodeDesc$4(componentLookup, affect.ref),
|
|
3922
|
+
startTime: scanTime
|
|
3923
|
+
})) : [createResult(ResultStatus.Failed, void 0, {
|
|
3924
|
+
codeDesc: `Vulnerability ${vuln.id}`,
|
|
3925
|
+
startTime: scanTime
|
|
3926
|
+
})];
|
|
3446
3927
|
const title = vuln.source?.name ? `${vuln.id} (${vuln.source.name})` : vuln.id;
|
|
3447
3928
|
const req = createRequirement(vuln.id, title, descriptions, impact, results, { tags });
|
|
3448
3929
|
const controlType = deriveControlTypeFromTags(nist);
|
|
@@ -3459,9 +3940,9 @@ async function convertCyclonedxToHdf(input) {
|
|
|
3459
3940
|
baselines: [baseline],
|
|
3460
3941
|
components: [{
|
|
3461
3942
|
name: targetName,
|
|
3462
|
-
type:
|
|
3943
|
+
type: TargetType.Application
|
|
3463
3944
|
}],
|
|
3464
|
-
timestamp:
|
|
3945
|
+
timestamp: scanTime
|
|
3465
3946
|
});
|
|
3466
3947
|
}
|
|
3467
3948
|
//#endregion
|
|
@@ -3632,7 +4113,7 @@ async function convertSplunkToHdf(input) {
|
|
|
3632
4113
|
const descriptions = convertDescriptions(control.descriptions);
|
|
3633
4114
|
const results = Array.isArray(control.results) ? control.results.map((result) => createResult(mapStatus$1(result.status), result.message, {
|
|
3634
4115
|
codeDesc: result.code_desc,
|
|
3635
|
-
startTime:
|
|
4116
|
+
startTime: parseTimestamp(result.start_time) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z"),
|
|
3636
4117
|
runTime: result.run_time,
|
|
3637
4118
|
exception: result.exception,
|
|
3638
4119
|
backtrace: result.backtrace
|
|
@@ -3655,11 +4136,11 @@ async function convertSplunkToHdf(input) {
|
|
|
3655
4136
|
allBaselines.push(createMinimalBaseline(profile.name, requirements, baselineOptions));
|
|
3656
4137
|
}
|
|
3657
4138
|
}
|
|
3658
|
-
|
|
4139
|
+
return serializeHdf({
|
|
3659
4140
|
baselines: allBaselines,
|
|
3660
4141
|
components: [{
|
|
3661
4142
|
name: targetName,
|
|
3662
|
-
type:
|
|
4143
|
+
type: TargetType.Host,
|
|
3663
4144
|
osName: targetRelease || void 0,
|
|
3664
4145
|
labels: {}
|
|
3665
4146
|
}],
|
|
@@ -3667,8 +4148,7 @@ async function convertSplunkToHdf(input) {
|
|
|
3667
4148
|
name: "splunk-to-hdf",
|
|
3668
4149
|
version: "1.0.0"
|
|
3669
4150
|
}
|
|
3670
|
-
};
|
|
3671
|
-
return JSON.stringify(hdf, null, 2);
|
|
4151
|
+
});
|
|
3672
4152
|
}
|
|
3673
4153
|
//#endregion
|
|
3674
4154
|
//#region converters/hdf-to-xml/typescript/converter.ts
|
|
@@ -3759,9 +4239,9 @@ function gitlabSeverityToImpact(severity) {
|
|
|
3759
4239
|
}
|
|
3760
4240
|
function scanTypeToTargetType(scanType) {
|
|
3761
4241
|
switch (scanType) {
|
|
3762
|
-
case "dast": return
|
|
3763
|
-
case "container_scanning": return
|
|
3764
|
-
default: return
|
|
4242
|
+
case "dast": return TargetType.Application;
|
|
4243
|
+
case "container_scanning": return TargetType.ContainerImage;
|
|
4244
|
+
default: return TargetType.Repository;
|
|
3765
4245
|
}
|
|
3766
4246
|
}
|
|
3767
4247
|
function scanTypeLabel(scanType) {
|
|
@@ -3878,7 +4358,7 @@ async function convertGitlabToHdf(input) {
|
|
|
3878
4358
|
const result = {
|
|
3879
4359
|
status: ResultStatus.Failed,
|
|
3880
4360
|
codeDesc: buildCodeDesc$4(scanType, vuln.location),
|
|
3881
|
-
startTime: startTime ? new Date(
|
|
4361
|
+
startTime: startTime ? parseTimestamp(startTime) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z") : /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
|
|
3882
4362
|
};
|
|
3883
4363
|
const impact = gitlabSeverityToImpact(vuln.severity ?? "Unknown");
|
|
3884
4364
|
const req = {
|
|
@@ -3894,9 +4374,14 @@ async function convertGitlabToHdf(input) {
|
|
|
3894
4374
|
if (controlType !== void 0) req.controlType = controlType;
|
|
3895
4375
|
requirements.push(req);
|
|
3896
4376
|
}
|
|
4377
|
+
const label = scanTypeLabel(scanType);
|
|
4378
|
+
if (requirements.length === 0) {
|
|
4379
|
+
const ts = startTime ? parseTimestamp(startTime) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date();
|
|
4380
|
+
requirements.push(buildNoFindingsRequirement("gitlab-no-findings", `GitLab ${label} scan via ${scannerName} reported zero findings.`, ts));
|
|
4381
|
+
}
|
|
3897
4382
|
const baseline = createMinimalBaseline("GitLab Security Scan", requirements, {
|
|
3898
4383
|
resultsChecksum,
|
|
3899
|
-
title: `GitLab ${
|
|
4384
|
+
title: `GitLab ${label} Security Scan`,
|
|
3900
4385
|
summary: `Scanner: ${scannerName}${scannerVersion ? ` v${scannerVersion}` : ""}`
|
|
3901
4386
|
});
|
|
3902
4387
|
const tool = {
|
|
@@ -3919,8 +4404,11 @@ async function convertGitlabToHdf(input) {
|
|
|
3919
4404
|
},
|
|
3920
4405
|
tool
|
|
3921
4406
|
};
|
|
3922
|
-
if (report.scan?.end_time)
|
|
3923
|
-
|
|
4407
|
+
if (report.scan?.end_time) {
|
|
4408
|
+
const endTime = parseTimestamp(report.scan.end_time);
|
|
4409
|
+
if (endTime) hdf.timestamp = endTime;
|
|
4410
|
+
}
|
|
4411
|
+
return serializeHdf(hdf);
|
|
3924
4412
|
}
|
|
3925
4413
|
//#endregion
|
|
3926
4414
|
//#region converters/trufflehog-to-hdf/typescript/converter.ts
|
|
@@ -3987,8 +4475,8 @@ function buildCodeDesc$3(f) {
|
|
|
3987
4475
|
*/
|
|
3988
4476
|
function getTimestamp(f) {
|
|
3989
4477
|
if (f.SourceMetadata?.Data?.Git?.timestamp) {
|
|
3990
|
-
const ts =
|
|
3991
|
-
if (
|
|
4478
|
+
const ts = parseTimestamp(f.SourceMetadata.Data.Git.timestamp);
|
|
4479
|
+
if (ts) return ts;
|
|
3992
4480
|
}
|
|
3993
4481
|
return /* @__PURE__ */ new Date();
|
|
3994
4482
|
}
|
|
@@ -4050,19 +4538,22 @@ async function convertTrufflehogToHdf(input) {
|
|
|
4050
4538
|
if (!input || input.trim().length === 0) throw new Error("trufflehog: empty input");
|
|
4051
4539
|
validateInputSize(input, "trufflehog");
|
|
4052
4540
|
const findings = parseFindings(input);
|
|
4053
|
-
if (findings.length === 0) throw new Error("trufflehog: no findings in input");
|
|
4054
4541
|
const resultsChecksum = await inputChecksum(input);
|
|
4055
4542
|
const limitedFindings = limitArrayWithWarning(findings, "finding");
|
|
4056
4543
|
const groups = groupFindings(limitedFindings);
|
|
4057
4544
|
const requirements = [];
|
|
4058
4545
|
for (const [reqID, group] of groups) requirements.push(buildRequirement$12(reqID, group));
|
|
4546
|
+
if (requirements.length === 0) {
|
|
4547
|
+
const target = findGitRepoURL(limitedFindings) ?? limitedFindings[0]?.SourceName ?? "the target source";
|
|
4548
|
+
requirements.push(buildNoFindingsRequirement("trufflehog-no-findings", `TruffleHog scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
4549
|
+
}
|
|
4059
4550
|
const hdf = {
|
|
4060
4551
|
baselines: [createMinimalBaseline("TruffleHog Scan", requirements, {
|
|
4061
4552
|
resultsChecksum,
|
|
4062
4553
|
title: `TruffleHog Scan (${limitedFindings[0]?.SourceName ?? "trufflehog"})`
|
|
4063
4554
|
})],
|
|
4064
4555
|
generator: {
|
|
4065
|
-
name: "hdf
|
|
4556
|
+
name: "trufflehog-to-hdf",
|
|
4066
4557
|
version: "1.0.0"
|
|
4067
4558
|
},
|
|
4068
4559
|
tool: {
|
|
@@ -4074,9 +4565,9 @@ async function convertTrufflehogToHdf(input) {
|
|
|
4074
4565
|
const repoURL = findGitRepoURL(limitedFindings);
|
|
4075
4566
|
if (repoURL) hdf.components = [{
|
|
4076
4567
|
name: repoURL,
|
|
4077
|
-
type:
|
|
4568
|
+
type: TargetType.Repository
|
|
4078
4569
|
}];
|
|
4079
|
-
return
|
|
4570
|
+
return serializeHdf(hdf);
|
|
4080
4571
|
}
|
|
4081
4572
|
//#endregion
|
|
4082
4573
|
//#region converters/burpsuite-to-hdf/typescript/converter.ts
|
|
@@ -4136,6 +4627,7 @@ async function convertBurpsuiteToHdf(input) {
|
|
|
4136
4627
|
const requirements = [];
|
|
4137
4628
|
for (const [issueType, groupIssues] of groups) requirements.push(buildRequirement$11(issueType, groupIssues));
|
|
4138
4629
|
const targetName = limitedIssues.length > 0 ? (limitedIssues[0].host?.["#text"] ?? "Unknown").trim() : "Unknown";
|
|
4630
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("burpsuite-no-findings", `Burp Suite scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
4139
4631
|
const baseline = createMinimalBaseline("BurpSuite Scan", requirements, {
|
|
4140
4632
|
resultsChecksum,
|
|
4141
4633
|
title: `BurpSuite Scan: ${targetName}`
|
|
@@ -4149,7 +4641,7 @@ async function convertBurpsuiteToHdf(input) {
|
|
|
4149
4641
|
baselines: [baseline],
|
|
4150
4642
|
components: [{
|
|
4151
4643
|
name: targetName,
|
|
4152
|
-
type:
|
|
4644
|
+
type: TargetType.Application
|
|
4153
4645
|
}],
|
|
4154
4646
|
generator: {
|
|
4155
4647
|
name: "burpsuite-to-hdf",
|
|
@@ -4157,8 +4649,8 @@ async function convertBurpsuiteToHdf(input) {
|
|
|
4157
4649
|
},
|
|
4158
4650
|
tool
|
|
4159
4651
|
};
|
|
4160
|
-
if (exportTime) hdf.timestamp =
|
|
4161
|
-
return
|
|
4652
|
+
if (exportTime) hdf.timestamp = parseTimestamp(exportTime) ?? void 0;
|
|
4653
|
+
return serializeHdf(hdf);
|
|
4162
4654
|
}
|
|
4163
4655
|
function buildRequirement$11(issueType, issues) {
|
|
4164
4656
|
const rep = issues[0];
|
|
@@ -4265,15 +4757,31 @@ function formatSummary(f) {
|
|
|
4265
4757
|
`IP Address, Port, Instance : ${f["IP Address, Port, Instance"] ?? ""}`
|
|
4266
4758
|
].join("\n");
|
|
4267
4759
|
}
|
|
4760
|
+
const MONTH_ABBR = {
|
|
4761
|
+
Jan: "01",
|
|
4762
|
+
Feb: "02",
|
|
4763
|
+
Mar: "03",
|
|
4764
|
+
Apr: "04",
|
|
4765
|
+
May: "05",
|
|
4766
|
+
Jun: "06",
|
|
4767
|
+
Jul: "07",
|
|
4768
|
+
Aug: "08",
|
|
4769
|
+
Sep: "09",
|
|
4770
|
+
Oct: "10",
|
|
4771
|
+
Nov: "11",
|
|
4772
|
+
Dec: "12"
|
|
4773
|
+
};
|
|
4268
4774
|
/**
|
|
4269
|
-
* Parses DBProtect date
|
|
4775
|
+
* Parses DBProtect date formats ("Feb 18 2021 15:57" or "2021-02-18 15:55").
|
|
4776
|
+
* Month-name values are normalized to ISO so parseTimestamp interprets them as
|
|
4777
|
+
* UTC, matching the Go peer and keeping output host-timezone-independent.
|
|
4270
4778
|
*/
|
|
4271
4779
|
function parseDate(dateStr) {
|
|
4272
4780
|
const trimmed = dateStr.trim();
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
return
|
|
4781
|
+
const ZERO = /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
|
|
4782
|
+
if (!trimmed) return ZERO;
|
|
4783
|
+
const month = /^([A-Z][a-z]{2}) (\d{1,2}) (\d{4}) (\d{2}:\d{2})$/.exec(trimmed);
|
|
4784
|
+
return parseTimestamp(month ? `${month[3]}-${MONTH_ABBR[month[1]] ?? "00"}-${month[2].padStart(2, "0")} ${month[4]}` : trimmed) ?? ZERO;
|
|
4277
4785
|
}
|
|
4278
4786
|
/**
|
|
4279
4787
|
* Builds a single EvaluatedRequirement from a group of findings sharing a Check ID.
|
|
@@ -4346,7 +4854,7 @@ async function convertDbprotectToHdf(input) {
|
|
|
4346
4854
|
})],
|
|
4347
4855
|
components: [{
|
|
4348
4856
|
name: targetName,
|
|
4349
|
-
type:
|
|
4857
|
+
type: TargetType.Host
|
|
4350
4858
|
}],
|
|
4351
4859
|
timestamp: /* @__PURE__ */ new Date()
|
|
4352
4860
|
});
|
|
@@ -4386,6 +4894,148 @@ function buildSummary(result) {
|
|
|
4386
4894
|
return `Package Vulnerability Summary: ${result.vulnerabilityDistribution ? String(result.vulnerabilityDistribution.total) : "N/A"} Application Compliance Issue Total: ${result.complianceDistribution ? String(result.complianceDistribution.total) : "N/A"}`;
|
|
4387
4895
|
}
|
|
4388
4896
|
/**
|
|
4897
|
+
* Detects CVSS schema version enum from the vector prefix. Defaults to 3.1
|
|
4898
|
+
* since modern Twistlock exclusively emits 3.x output.
|
|
4899
|
+
*/
|
|
4900
|
+
function cvssVersionFromVector(vector) {
|
|
4901
|
+
if (!vector) return Version.The31;
|
|
4902
|
+
if (vector.startsWith("CVSS:2.0/")) return Version.The20;
|
|
4903
|
+
if (vector.startsWith("CVSS:3.0/")) return Version.The30;
|
|
4904
|
+
if (vector.startsWith("CVSS:4.0/")) return Version.The40;
|
|
4905
|
+
return Version.The31;
|
|
4906
|
+
}
|
|
4907
|
+
/**
|
|
4908
|
+
* Maps cvssScoreToSeverity('low'|'medium'|...) into the CVSSSeverity enum.
|
|
4909
|
+
*/
|
|
4910
|
+
function cvssSeverityFromScore(score) {
|
|
4911
|
+
switch (cvssScoreToSeverity(score)) {
|
|
4912
|
+
case "none": return CVSSSeverity.None;
|
|
4913
|
+
case "low": return CVSSSeverity.Low;
|
|
4914
|
+
case "medium": return CVSSSeverity.Medium;
|
|
4915
|
+
case "high": return CVSSSeverity.High;
|
|
4916
|
+
case "critical": return CVSSSeverity.Critical;
|
|
4917
|
+
default: return;
|
|
4918
|
+
}
|
|
4919
|
+
}
|
|
4920
|
+
/**
|
|
4921
|
+
* Builds a Cvss entry from a Twistlock vulnerability. Returns undefined only
|
|
4922
|
+
* when neither a score nor a vector is available. When the vendor emits a
|
|
4923
|
+
* score but no vector (common in Twistlock/Prisma Cloud output), the Cvss
|
|
4924
|
+
* entry is still emitted — the schema makes baseVector optional precisely
|
|
4925
|
+
* so vendor-final-score data isn't dropped.
|
|
4926
|
+
*/
|
|
4927
|
+
function buildCvss(vuln) {
|
|
4928
|
+
const hasScore = typeof vuln.cvss === "number" && Number.isFinite(vuln.cvss);
|
|
4929
|
+
const hasVector = !!vuln.vector;
|
|
4930
|
+
if (!hasScore && !hasVector) return void 0;
|
|
4931
|
+
const cv = { version: cvssVersionFromVector(vuln.vector) };
|
|
4932
|
+
if (hasScore) {
|
|
4933
|
+
cv.baseScore = vuln.cvss;
|
|
4934
|
+
const sev = cvssSeverityFromScore(vuln.cvss);
|
|
4935
|
+
if (sev !== void 0) cv.baseSeverity = sev;
|
|
4936
|
+
}
|
|
4937
|
+
if (hasVector) cv.baseVector = vuln.vector;
|
|
4938
|
+
const source = vuln.cve ?? vuln.id;
|
|
4939
|
+
if (source) cv.source = source;
|
|
4940
|
+
return cv;
|
|
4941
|
+
}
|
|
4942
|
+
const CWE_REGEX = /cwe[-_]?(\d+)/gi;
|
|
4943
|
+
/**
|
|
4944
|
+
* Extracts canonical CWE-N identifiers from a free-form string.
|
|
4945
|
+
*/
|
|
4946
|
+
function parseCwes(raw) {
|
|
4947
|
+
if (!raw) return [];
|
|
4948
|
+
const out = [];
|
|
4949
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4950
|
+
for (const m of raw.matchAll(CWE_REGEX)) {
|
|
4951
|
+
const id = `CWE-${m[1]}`;
|
|
4952
|
+
if (seen.has(id)) continue;
|
|
4953
|
+
seen.add(id);
|
|
4954
|
+
out.push(id);
|
|
4955
|
+
}
|
|
4956
|
+
return out;
|
|
4957
|
+
}
|
|
4958
|
+
function rhelDistro(distro) {
|
|
4959
|
+
const low = distro.toLowerCase();
|
|
4960
|
+
return [
|
|
4961
|
+
"red hat",
|
|
4962
|
+
"rhel",
|
|
4963
|
+
"centos",
|
|
4964
|
+
"fedora",
|
|
4965
|
+
"amazon linux",
|
|
4966
|
+
"oracle linux",
|
|
4967
|
+
"rocky",
|
|
4968
|
+
"alma"
|
|
4969
|
+
].some((m) => low.includes(m));
|
|
4970
|
+
}
|
|
4971
|
+
function debDistro(distro) {
|
|
4972
|
+
const low = distro.toLowerCase();
|
|
4973
|
+
return ["debian", "ubuntu"].some((m) => low.includes(m));
|
|
4974
|
+
}
|
|
4975
|
+
/**
|
|
4976
|
+
* Maps a Twistlock package type plus the result's distro string to a schema
|
|
4977
|
+
* Ecosystem value. Defaults to 'generic' for unknown types.
|
|
4978
|
+
*/
|
|
4979
|
+
function resolveEcosystem(packageType, distro) {
|
|
4980
|
+
const t = (packageType ?? "").toLowerCase();
|
|
4981
|
+
const d = distro ?? "";
|
|
4982
|
+
switch (t) {
|
|
4983
|
+
case "os":
|
|
4984
|
+
if (rhelDistro(d)) return Ecosystem.RPM;
|
|
4985
|
+
if (debDistro(d)) return Ecosystem.Deb;
|
|
4986
|
+
return Ecosystem.Generic;
|
|
4987
|
+
case "rpm": return Ecosystem.RPM;
|
|
4988
|
+
case "deb": return Ecosystem.Deb;
|
|
4989
|
+
case "jar":
|
|
4990
|
+
case "maven": return Ecosystem.Maven;
|
|
4991
|
+
case "python":
|
|
4992
|
+
case "pypi": return Ecosystem.Pypi;
|
|
4993
|
+
case "nodejs":
|
|
4994
|
+
case "npm": return Ecosystem.Npm;
|
|
4995
|
+
case "gem": return Ecosystem.Gem;
|
|
4996
|
+
case "nuget": return Ecosystem.Nuget;
|
|
4997
|
+
case "go": return Ecosystem.Go;
|
|
4998
|
+
case "cargo": return Ecosystem.Cargo;
|
|
4999
|
+
default: return Ecosystem.Generic;
|
|
5000
|
+
}
|
|
5001
|
+
}
|
|
5002
|
+
const FIX_VERSION_REGEX = /\d+(?:\.\d+)+[A-Za-z0-9._+\-]*/;
|
|
5003
|
+
/**
|
|
5004
|
+
* Extracts the first version-looking token from fixedBy or a "fixed in X"
|
|
5005
|
+
* status string. Returns empty string when no fix info is present.
|
|
5006
|
+
*/
|
|
5007
|
+
function extractFixedInVersion(vuln) {
|
|
5008
|
+
if (vuln.fixedBy) return vuln.fixedBy;
|
|
5009
|
+
if (!(vuln.status ?? "").toLowerCase().includes("fixed")) return "";
|
|
5010
|
+
const m = (vuln.status ?? "").match(FIX_VERSION_REGEX);
|
|
5011
|
+
return m ? m[0] : "";
|
|
5012
|
+
}
|
|
5013
|
+
/**
|
|
5014
|
+
* Builds an AffectedPackage entry from per-vulnerability fields. Returns
|
|
5015
|
+
* undefined when packageName or packageVersion are missing (both required).
|
|
5016
|
+
*/
|
|
5017
|
+
function buildAffectedPackage(vuln, packageTypes, distro) {
|
|
5018
|
+
if (!vuln.packageName || !vuln.packageVersion) return void 0;
|
|
5019
|
+
const pkgType = vuln.packageType ?? packageTypes.get(vuln.packageName);
|
|
5020
|
+
const pkg = {
|
|
5021
|
+
name: vuln.packageName,
|
|
5022
|
+
version: vuln.packageVersion,
|
|
5023
|
+
ecosystem: resolveEcosystem(pkgType, distro)
|
|
5024
|
+
};
|
|
5025
|
+
const fixed = extractFixedInVersion(vuln);
|
|
5026
|
+
if (fixed) pkg.fixedInVersion = fixed;
|
|
5027
|
+
return pkg;
|
|
5028
|
+
}
|
|
5029
|
+
/**
|
|
5030
|
+
* Indexes packageName → packageType from the result-level packages array.
|
|
5031
|
+
*/
|
|
5032
|
+
function buildPackageTypeIndex(pkgs) {
|
|
5033
|
+
const idx = /* @__PURE__ */ new Map();
|
|
5034
|
+
if (!pkgs) return idx;
|
|
5035
|
+
for (const p of pkgs) if (p.name && p.type) idx.set(p.name, p.type);
|
|
5036
|
+
return idx;
|
|
5037
|
+
}
|
|
5038
|
+
/**
|
|
4389
5039
|
* Builds the code_desc string for a vulnerability result.
|
|
4390
5040
|
*/
|
|
4391
5041
|
function formatCodeDesc$2(vuln) {
|
|
@@ -4395,15 +5045,22 @@ function formatCodeDesc$2(vuln) {
|
|
|
4395
5045
|
}
|
|
4396
5046
|
/**
|
|
4397
5047
|
* Converts a single vulnerability into an EvaluatedRequirement.
|
|
5048
|
+
*
|
|
5049
|
+
* @param vuln - The Twistlock vulnerability object
|
|
5050
|
+
* @param packageTypes - name→type lookup built from the enclosing result's packages array
|
|
5051
|
+
* @param distro - the enclosing result's distro string, used to disambiguate "os" packages
|
|
4398
5052
|
*/
|
|
4399
|
-
function buildRequirement$9(vuln) {
|
|
5053
|
+
function buildRequirement$9(vuln, packageTypes, distro) {
|
|
4400
5054
|
const nist = DEFAULT_REMEDIATION_NIST_TAGS;
|
|
4401
|
-
const
|
|
5055
|
+
const cciTags = nistToCci(nist);
|
|
5056
|
+
const extras = { cveid: [vuln.id] };
|
|
5057
|
+
if (typeof vuln.cvss === "number" && vuln.cvss > 0) extras["cvss_base_score"] = vuln.cvss;
|
|
5058
|
+
const tags = buildNistCciTags(nist, cciTags, extras);
|
|
4402
5059
|
const descriptions = [{
|
|
4403
5060
|
label: "default",
|
|
4404
5061
|
data: vuln.description
|
|
4405
5062
|
}];
|
|
4406
|
-
const startTime = vuln.discoveredDate ?
|
|
5063
|
+
const startTime = (vuln.discoveredDate ? parseTimestamp(vuln.discoveredDate) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
|
|
4407
5064
|
const results = [createResult(ResultStatus.Failed, void 0, {
|
|
4408
5065
|
codeDesc: formatCodeDesc$2(vuln),
|
|
4409
5066
|
startTime
|
|
@@ -4412,6 +5069,12 @@ function buildRequirement$9(vuln) {
|
|
|
4412
5069
|
const controlType = deriveControlTypeFromTags(nist);
|
|
4413
5070
|
if (controlType !== void 0) req.controlType = controlType;
|
|
4414
5071
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
5072
|
+
const cv = buildCvss(vuln);
|
|
5073
|
+
if (cv) req.cvss = [cv];
|
|
5074
|
+
const cwes = parseCwes(vuln.cwe);
|
|
5075
|
+
if (cwes.length > 0) req.cwe = cwes;
|
|
5076
|
+
const pkg = buildAffectedPackage(vuln, packageTypes, distro);
|
|
5077
|
+
if (pkg) req.affectedPackages = [pkg];
|
|
4415
5078
|
return req;
|
|
4416
5079
|
}
|
|
4417
5080
|
/**
|
|
@@ -4422,7 +5085,13 @@ function convertSingleResult(result, resultsChecksum) {
|
|
|
4422
5085
|
const { items: limitedVulns, truncated } = limitArray(vulns);
|
|
4423
5086
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
4424
5087
|
if (truncated) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${vulns.length})`);
|
|
4425
|
-
|
|
5088
|
+
const packageTypes = buildPackageTypeIndex(result.packages);
|
|
5089
|
+
const requirements = limitedVulns.map((vuln) => buildRequirement$9(vuln, packageTypes, result.distro));
|
|
5090
|
+
if (requirements.length === 0) {
|
|
5091
|
+
const target = result.name ?? result.repository ?? result.id ?? "scan target";
|
|
5092
|
+
requirements.push(buildNoFindingsRequirement("twistlock-no-findings", `Twistlock scanned ${target} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
|
|
5093
|
+
}
|
|
5094
|
+
return createMinimalBaseline("Twistlock Scan", requirements, {
|
|
4426
5095
|
resultsChecksum,
|
|
4427
5096
|
title: buildTitle$1(result),
|
|
4428
5097
|
summary: buildSummary(result)
|
|
@@ -4457,7 +5126,7 @@ async function convertTwistlockToHdf(input) {
|
|
|
4457
5126
|
baselines,
|
|
4458
5127
|
components: [{
|
|
4459
5128
|
name: targetName,
|
|
4460
|
-
type:
|
|
5129
|
+
type: TargetType.ContainerImage,
|
|
4461
5130
|
labels: { image: results[0]?.id ?? targetName }
|
|
4462
5131
|
}],
|
|
4463
5132
|
timestamp: /* @__PURE__ */ new Date()
|
|
@@ -4523,15 +5192,36 @@ function buildRequirement$8(finding, timestamp) {
|
|
|
4523
5192
|
const codeDesc = finding.vulnerability.recommendation ?? "No recommendation available";
|
|
4524
5193
|
const results = [createResult(ResultStatus.Failed, void 0, {
|
|
4525
5194
|
codeDesc,
|
|
4526
|
-
startTime: timestamp ?
|
|
5195
|
+
startTime: (timestamp ? parseTimestamp(timestamp) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
|
|
4527
5196
|
})];
|
|
4528
5197
|
const req = createRequirement(finding.matrix, getTitle$1(finding), descriptions, getImpact$5(finding.vulnerability.severity), results, { tags });
|
|
4529
5198
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
4530
5199
|
const controlType = deriveControlTypeFromTags(nist);
|
|
4531
5200
|
if (controlType !== void 0) req.controlType = controlType;
|
|
5201
|
+
const pkg = buildAffectedPackageFromComponent(finding.component);
|
|
5202
|
+
if (pkg) req.affectedPackages = [pkg];
|
|
4532
5203
|
return req;
|
|
4533
5204
|
}
|
|
4534
5205
|
/**
|
|
5206
|
+
* Builds an Affected_Package from a Dependency-Track component. Prefers
|
|
5207
|
+
* the rich identifiers Dependency-Track already exposes (purl, cpe) and
|
|
5208
|
+
* augments with name/version/ecosystem when available. Returns undefined
|
|
5209
|
+
* when the component carries no schema-acceptable identifier.
|
|
5210
|
+
*/
|
|
5211
|
+
function buildAffectedPackageFromComponent(c) {
|
|
5212
|
+
let ecosystem;
|
|
5213
|
+
if (c.purl) ecosystem = ecosystemFromPurlType(parsePurl(c.purl)?.type);
|
|
5214
|
+
else if (c.name && c.version) ecosystem = ecosystemFromPurlType(void 0);
|
|
5215
|
+
return buildAffectedPackage$1({
|
|
5216
|
+
name: c.name,
|
|
5217
|
+
version: c.version,
|
|
5218
|
+
ecosystem,
|
|
5219
|
+
purl: c.purl,
|
|
5220
|
+
cpe: c.cpe,
|
|
5221
|
+
fixedInVersion: c.latestVersion
|
|
5222
|
+
});
|
|
5223
|
+
}
|
|
5224
|
+
/**
|
|
4535
5225
|
* Converts Dependency-Track FPF JSON output to HDF format.
|
|
4536
5226
|
*
|
|
4537
5227
|
* @param input - Dependency-Track FPF JSON string
|
|
@@ -4544,7 +5234,12 @@ async function convertDeptrackToHdf(input) {
|
|
|
4544
5234
|
const parsed = parseJSON(input);
|
|
4545
5235
|
if (!parsed || typeof parsed !== "object") throw new Error("deptrack: invalid JSON");
|
|
4546
5236
|
if (!parsed.findings && !parsed.project && !parsed.meta) throw new Error("deptrack: input does not appear to be a Dependency-Track report");
|
|
4547
|
-
const
|
|
5237
|
+
const requirements = (parsed.findings ?? []).map((finding) => buildRequirement$8(finding, parsed.meta?.timestamp));
|
|
5238
|
+
if (requirements.length === 0) {
|
|
5239
|
+
const projectName = parsed.project?.name ?? parsed.project?.uuid ?? "";
|
|
5240
|
+
requirements.push(buildNoFindingsRequirement("deptrack-no-findings", `Dependency-Track analyzed ${projectName} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
|
|
5241
|
+
}
|
|
5242
|
+
const baseline = createMinimalBaseline("Dependency-Track Scan", requirements, {
|
|
4548
5243
|
resultsChecksum,
|
|
4549
5244
|
title: `Dependency-Track: ${parsed.project?.name ?? ""} ${parsed.project?.version ?? ""}`,
|
|
4550
5245
|
summary: parsed.project?.description
|
|
@@ -4558,7 +5253,7 @@ async function convertDeptrackToHdf(input) {
|
|
|
4558
5253
|
baselines: [baseline],
|
|
4559
5254
|
components: [{
|
|
4560
5255
|
name: targetName,
|
|
4561
|
-
type:
|
|
5256
|
+
type: TargetType.Application
|
|
4562
5257
|
}],
|
|
4563
5258
|
timestamp: /* @__PURE__ */ new Date()
|
|
4564
5259
|
});
|
|
@@ -4618,7 +5313,7 @@ function formatCodeDesc$1(entry) {
|
|
|
4618
5313
|
/**
|
|
4619
5314
|
* Builds a single EvaluatedRequirement from a group of entries sharing an ID.
|
|
4620
5315
|
*/
|
|
4621
|
-
function buildRequirement$7(entryID, entries) {
|
|
5316
|
+
function buildRequirement$7(entryID, entries, scanTime) {
|
|
4622
5317
|
const rep = entries[0];
|
|
4623
5318
|
const cweIDs = extractCWEs$1(rep);
|
|
4624
5319
|
const nist = mapCWEToNIST(cweIDs, DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
|
|
@@ -4631,13 +5326,57 @@ function buildRequirement$7(entryID, entries) {
|
|
|
4631
5326
|
label: "default",
|
|
4632
5327
|
data: formatDescription(rep)
|
|
4633
5328
|
}];
|
|
4634
|
-
const results = entries.map((entry) => createResult(ResultStatus.Failed, void 0, {
|
|
5329
|
+
const results = entries.map((entry) => createResult(ResultStatus.Failed, void 0, {
|
|
5330
|
+
codeDesc: formatCodeDesc$1(entry),
|
|
5331
|
+
startTime: scanTime
|
|
5332
|
+
}));
|
|
4635
5333
|
const req = createRequirement(entryID, rep.summary, descriptions, severityToImpact(rep.severity), results, { tags });
|
|
4636
5334
|
const controlType = deriveControlTypeFromTags(nist);
|
|
4637
5335
|
if (controlType !== void 0) req.controlType = controlType;
|
|
4638
5336
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
5337
|
+
const pkg = buildAffectedPackageFromEntry(rep);
|
|
5338
|
+
if (pkg) req.affectedPackages = [pkg];
|
|
4639
5339
|
return req;
|
|
4640
5340
|
}
|
|
5341
|
+
function ecosystemFromXraySource(scheme) {
|
|
5342
|
+
if (scheme === "gav") return Ecosystem.Maven;
|
|
5343
|
+
return ecosystemFromPurlType(scheme);
|
|
5344
|
+
}
|
|
5345
|
+
function parseSourceCompID(s) {
|
|
5346
|
+
if (!s) return void 0;
|
|
5347
|
+
const m = /^([a-zA-Z0-9]+):\/\/(.+)$/.exec(s);
|
|
5348
|
+
if (!m) return void 0;
|
|
5349
|
+
const scheme = m[1].toLowerCase();
|
|
5350
|
+
const rest = m[2];
|
|
5351
|
+
const colonIdx = rest.lastIndexOf(":");
|
|
5352
|
+
if (colonIdx > 0) return {
|
|
5353
|
+
scheme,
|
|
5354
|
+
name: rest.slice(0, colonIdx),
|
|
5355
|
+
version: rest.slice(colonIdx + 1)
|
|
5356
|
+
};
|
|
5357
|
+
return {
|
|
5358
|
+
scheme,
|
|
5359
|
+
name: rest
|
|
5360
|
+
};
|
|
5361
|
+
}
|
|
5362
|
+
function buildAffectedPackageFromEntry(entry) {
|
|
5363
|
+
const parsed = parseSourceCompID(entry.source_comp_id ?? entry.source_id);
|
|
5364
|
+
let name = entry.component ?? "";
|
|
5365
|
+
let version;
|
|
5366
|
+
let ecosystem;
|
|
5367
|
+
if (parsed) {
|
|
5368
|
+
if (parsed.name) name = parsed.name;
|
|
5369
|
+
version = parsed.version;
|
|
5370
|
+
ecosystem = ecosystemFromXraySource(parsed.scheme);
|
|
5371
|
+
}
|
|
5372
|
+
const fixed = entry.component_versions?.fixed_versions?.[0];
|
|
5373
|
+
return buildAffectedPackage$1({
|
|
5374
|
+
name,
|
|
5375
|
+
version,
|
|
5376
|
+
ecosystem: ecosystem ?? (name ? Ecosystem.Generic : void 0),
|
|
5377
|
+
fixedInVersion: fixed
|
|
5378
|
+
});
|
|
5379
|
+
}
|
|
4641
5380
|
/**
|
|
4642
5381
|
* Converts JFrog Xray JSON output to HDF format.
|
|
4643
5382
|
*
|
|
@@ -4650,6 +5389,7 @@ async function convertJfrogXrayToHdf(input) {
|
|
|
4650
5389
|
const resultsChecksum = await inputChecksum(input);
|
|
4651
5390
|
const parsed = parseJSON(input);
|
|
4652
5391
|
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.data)) throw new Error("jfrog-xray: invalid JSON structure");
|
|
5392
|
+
const scanTime = /* @__PURE__ */ new Date();
|
|
4653
5393
|
const { items: limitedEntries, truncated } = limitArray(parsed.data);
|
|
4654
5394
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
4655
5395
|
if (truncated) console.warn(`WARNING: Input truncated at ${limitedEntries.length} entries (original: ${parsed.data.length})`);
|
|
@@ -4666,7 +5406,8 @@ async function convertJfrogXrayToHdf(input) {
|
|
|
4666
5406
|
else groups.set(id, [entry]);
|
|
4667
5407
|
}
|
|
4668
5408
|
const requirements = [];
|
|
4669
|
-
for (const [entryID, entries] of groups) requirements.push(buildRequirement$7(entryID, entries));
|
|
5409
|
+
for (const [entryID, entries] of groups) requirements.push(buildRequirement$7(entryID, entries, scanTime));
|
|
5410
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("jfrog-xray-no-findings", "JFrog Xray scanned the target artifact and reported zero vulnerable components.", scanTime));
|
|
4670
5411
|
return buildHdfResults({
|
|
4671
5412
|
generatorName: "jfrog-xray-to-hdf",
|
|
4672
5413
|
converterVersion: "1.0.0",
|
|
@@ -4675,9 +5416,9 @@ async function convertJfrogXrayToHdf(input) {
|
|
|
4675
5416
|
baselines: [createMinimalBaseline("JFrog Xray Scan", requirements, { resultsChecksum })],
|
|
4676
5417
|
components: [{
|
|
4677
5418
|
name: "JFrog Xray Scan",
|
|
4678
|
-
type:
|
|
5419
|
+
type: TargetType.Application
|
|
4679
5420
|
}],
|
|
4680
|
-
timestamp:
|
|
5421
|
+
timestamp: scanTime
|
|
4681
5422
|
});
|
|
4682
5423
|
}
|
|
4683
5424
|
//#endregion
|
|
@@ -4721,7 +5462,7 @@ function vulnMessage(vuln) {
|
|
|
4721
5462
|
/**
|
|
4722
5463
|
* Builds a single EvaluatedRequirement from a NeuVector vulnerability.
|
|
4723
5464
|
*/
|
|
4724
|
-
function buildRequirement$6(vuln) {
|
|
5465
|
+
function buildRequirement$6(vuln, scanTime) {
|
|
4725
5466
|
const cweIDs = extractCWEs(vuln.description);
|
|
4726
5467
|
const nist = mapCWEToNIST(cweIDs, DEFAULT_REMEDIATION_NIST_TAGS);
|
|
4727
5468
|
const tags = {
|
|
@@ -4733,11 +5474,23 @@ function buildRequirement$6(vuln) {
|
|
|
4733
5474
|
label: "default",
|
|
4734
5475
|
data: vuln.description
|
|
4735
5476
|
}];
|
|
4736
|
-
const results = [createResult(ResultStatus.Failed, vulnMessage(vuln), {
|
|
5477
|
+
const results = [createResult(ResultStatus.Failed, vulnMessage(vuln), {
|
|
5478
|
+
codeDesc: "",
|
|
5479
|
+
startTime: scanTime
|
|
5480
|
+
})];
|
|
4737
5481
|
const req = createRequirement(vulnID(vuln), vulnTitle(vuln), descriptions, getImpact$4(vuln), results, { tags });
|
|
4738
5482
|
const controlType = deriveControlTypeFromTags(nist);
|
|
4739
5483
|
if (controlType !== void 0) req.controlType = controlType;
|
|
4740
5484
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
5485
|
+
const cpe23 = (vuln.cpes ?? []).find((c) => /^cpe:2\.3:[aho]:/.test(c));
|
|
5486
|
+
const pkg = buildAffectedPackage$1({
|
|
5487
|
+
name: vuln.package_name,
|
|
5488
|
+
version: vuln.package_version,
|
|
5489
|
+
ecosystem: vuln.package_name && vuln.package_version ? Ecosystem.Generic : void 0,
|
|
5490
|
+
cpe: cpe23,
|
|
5491
|
+
fixedInVersion: vuln.fixed_version
|
|
5492
|
+
});
|
|
5493
|
+
if (pkg) req.affectedPackages = [pkg];
|
|
4741
5494
|
return req;
|
|
4742
5495
|
}
|
|
4743
5496
|
/**
|
|
@@ -4775,14 +5528,17 @@ async function convertNeuvectorToHdf(input) {
|
|
|
4775
5528
|
const { items: limitedVulns, truncated } = limitArray(scan.report.vulnerabilities ?? []);
|
|
4776
5529
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
4777
5530
|
if (truncated) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${scan.report.vulnerabilities.length})`);
|
|
5531
|
+
const scanTime = /* @__PURE__ */ new Date();
|
|
4778
5532
|
const seen = /* @__PURE__ */ new Set();
|
|
4779
5533
|
const requirements = [];
|
|
4780
5534
|
for (const vuln of limitedVulns) {
|
|
4781
5535
|
const id = vulnID(vuln);
|
|
4782
5536
|
if (seen.has(id)) continue;
|
|
4783
5537
|
seen.add(id);
|
|
4784
|
-
requirements.push(buildRequirement$6(vuln));
|
|
5538
|
+
requirements.push(buildRequirement$6(vuln, scanTime));
|
|
4785
5539
|
}
|
|
5540
|
+
const target = targetNameFromReport(scan.report);
|
|
5541
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("neuvector-no-findings", `NeuVector scanned ${target} and reported zero vulnerable components.`, scanTime));
|
|
4786
5542
|
return buildHdfResults({
|
|
4787
5543
|
generatorName: "neuvector-to-hdf",
|
|
4788
5544
|
converterVersion: "1.0.0",
|
|
@@ -4794,13 +5550,13 @@ async function convertNeuvectorToHdf(input) {
|
|
|
4794
5550
|
})],
|
|
4795
5551
|
components: [{
|
|
4796
5552
|
name: targetNameFromReport(scan.report),
|
|
4797
|
-
type:
|
|
5553
|
+
type: TargetType.ContainerImage,
|
|
4798
5554
|
labels: {
|
|
4799
5555
|
image: `${scan.report.registry}/${scan.report.repository}:${scan.report.tag}`,
|
|
4800
5556
|
registry: scan.report.registry
|
|
4801
5557
|
}
|
|
4802
5558
|
}],
|
|
4803
|
-
timestamp:
|
|
5559
|
+
timestamp: scanTime
|
|
4804
5560
|
});
|
|
4805
5561
|
}
|
|
4806
5562
|
//#endregion
|
|
@@ -4875,7 +5631,7 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
|
|
|
4875
5631
|
const results = limitedVulns.map((vuln) => ({
|
|
4876
5632
|
status: ResultStatus.Failed,
|
|
4877
5633
|
codeDesc: buildCodeDesc$2(vuln, snippetMap),
|
|
4878
|
-
startTime: new Date(
|
|
5634
|
+
startTime: parseTimestamp(startTimeStr) ?? /* @__PURE__ */ new Date()
|
|
4879
5635
|
}));
|
|
4880
5636
|
const req = {
|
|
4881
5637
|
id: desc.classID ?? "unknown",
|
|
@@ -4915,21 +5671,22 @@ async function convertFortifyToHdf(input) {
|
|
|
4915
5671
|
if (truncatedDescs) console.warn(`WARNING: Input truncated at ${limitedDescs.length} Description items (original: ${descriptions.length})`);
|
|
4916
5672
|
const createdDate = fvdl.CreatedTS?.date ?? "";
|
|
4917
5673
|
const startTimeStr = `${createdDate}T${fvdl.CreatedTS?.time ?? ""}`;
|
|
4918
|
-
const
|
|
5674
|
+
const requirements = limitedDescs.map((desc) => {
|
|
4919
5675
|
return buildRequirement$5(desc, vulnGroups.get(desc.classID ?? "") ?? [], snippetMap, startTimeStr);
|
|
4920
|
-
}), {
|
|
4921
|
-
resultsChecksum,
|
|
4922
|
-
title: "Fortify Static Analyzer Scan",
|
|
4923
|
-
summary: `Fortify Static Analyzer Scan of UUID: ${fvdl.UUID ?? ""}`,
|
|
4924
|
-
version: fvdl.EngineData?.EngineVersion ?? "",
|
|
4925
|
-
status: "loaded"
|
|
4926
5676
|
});
|
|
4927
5677
|
const targetName = fvdl.Build?.SourceBasePath ?? fvdl.Build?.BuildID ?? "Unknown";
|
|
5678
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("fortify-no-findings", `Fortify scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
4928
5679
|
const hdfResult = {
|
|
4929
|
-
baselines: [
|
|
5680
|
+
baselines: [createMinimalBaseline("Fortify Scan", requirements, {
|
|
5681
|
+
resultsChecksum,
|
|
5682
|
+
title: "Fortify Static Analyzer Scan",
|
|
5683
|
+
summary: `Fortify Static Analyzer Scan of UUID: ${fvdl.UUID ?? ""}`,
|
|
5684
|
+
version: fvdl.EngineData?.EngineVersion ?? "",
|
|
5685
|
+
status: "loaded"
|
|
5686
|
+
})],
|
|
4930
5687
|
components: [{
|
|
4931
5688
|
name: targetName,
|
|
4932
|
-
type:
|
|
5689
|
+
type: TargetType.Repository
|
|
4933
5690
|
}],
|
|
4934
5691
|
generator: {
|
|
4935
5692
|
name: "fortify-to-hdf",
|
|
@@ -4940,8 +5697,8 @@ async function convertFortifyToHdf(input) {
|
|
|
4940
5697
|
format: "FVDL"
|
|
4941
5698
|
}
|
|
4942
5699
|
};
|
|
4943
|
-
if (createdDate) hdfResult.timestamp = new Date(
|
|
4944
|
-
return
|
|
5700
|
+
if (createdDate) hdfResult.timestamp = parseTimestamp(startTimeStr) ?? /* @__PURE__ */ new Date();
|
|
5701
|
+
return serializeHdf(hdfResult);
|
|
4945
5702
|
}
|
|
4946
5703
|
//#endregion
|
|
4947
5704
|
//#region converters/prisma-to-hdf/typescript/converter.ts
|
|
@@ -5025,7 +5782,7 @@ function makeTitle(rec) {
|
|
|
5025
5782
|
/**
|
|
5026
5783
|
* Builds a single EvaluatedRequirement from a Prisma record.
|
|
5027
5784
|
*/
|
|
5028
|
-
function buildRequirement$4(rec) {
|
|
5785
|
+
function buildRequirement$4(rec, scanTime) {
|
|
5029
5786
|
const id = makeRequirementID(rec);
|
|
5030
5787
|
const title = makeTitle(rec);
|
|
5031
5788
|
const codeDesc = makeCodeDesc(rec);
|
|
@@ -5039,14 +5796,58 @@ function buildRequirement$4(rec) {
|
|
|
5039
5796
|
label: "default",
|
|
5040
5797
|
data: rec.Description
|
|
5041
5798
|
}];
|
|
5042
|
-
const results = [createResult(ResultStatus.Failed, message, {
|
|
5799
|
+
const results = [createResult(ResultStatus.Failed, message, {
|
|
5800
|
+
codeDesc,
|
|
5801
|
+
startTime: scanTime
|
|
5802
|
+
})];
|
|
5043
5803
|
const req = createRequirement(id, title, descriptions, getImpact$3(rec.Severity), results, { tags });
|
|
5044
5804
|
const controlType = deriveControlTypeFromTags(nist);
|
|
5045
5805
|
if (controlType !== void 0) req.controlType = controlType;
|
|
5046
5806
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
5807
|
+
if (rec["CVE ID"]) {
|
|
5808
|
+
const pkg = buildAffectedPackageFromRecord(rec);
|
|
5809
|
+
if (pkg) req.affectedPackages = [pkg];
|
|
5810
|
+
}
|
|
5047
5811
|
return req;
|
|
5048
5812
|
}
|
|
5049
5813
|
/**
|
|
5814
|
+
* Distro slugs in Prisma look like `redhat-RHEL7`, `debian-buster`,
|
|
5815
|
+
* `alpine-3.14`, `ubuntu-20.04`. Only the leading vendor segment is
|
|
5816
|
+
* mapped — unknown vendors fall back to `generic` rather than guessing.
|
|
5817
|
+
*/
|
|
5818
|
+
function ecosystemFromDistro(distro) {
|
|
5819
|
+
if (!distro) return Ecosystem.Generic;
|
|
5820
|
+
switch (distro.split("-")[0]?.toLowerCase()) {
|
|
5821
|
+
case "redhat":
|
|
5822
|
+
case "rhel":
|
|
5823
|
+
case "centos":
|
|
5824
|
+
case "rocky":
|
|
5825
|
+
case "alma":
|
|
5826
|
+
case "fedora":
|
|
5827
|
+
case "amazon":
|
|
5828
|
+
case "amazonlinux":
|
|
5829
|
+
case "suse":
|
|
5830
|
+
case "sles":
|
|
5831
|
+
case "opensuse": return Ecosystem.RPM;
|
|
5832
|
+
case "debian":
|
|
5833
|
+
case "ubuntu": return Ecosystem.Deb;
|
|
5834
|
+
default: return Ecosystem.Generic;
|
|
5835
|
+
}
|
|
5836
|
+
}
|
|
5837
|
+
const FIX_VERSION_PATTERN = /fixed in\s+([^\s,;]+)/i;
|
|
5838
|
+
function buildAffectedPackageFromRecord(rec) {
|
|
5839
|
+
const name = rec["Source Package"] || rec.Packages;
|
|
5840
|
+
const version = rec["Package Version"];
|
|
5841
|
+
if (!name || !version) return void 0;
|
|
5842
|
+
const fixMatch = rec["Fix Status"] ? FIX_VERSION_PATTERN.exec(rec["Fix Status"]) : null;
|
|
5843
|
+
return buildAffectedPackage$1({
|
|
5844
|
+
name,
|
|
5845
|
+
version,
|
|
5846
|
+
ecosystem: ecosystemFromDistro(rec.Distro),
|
|
5847
|
+
fixedInVersion: fixMatch ? fixMatch[1] : void 0
|
|
5848
|
+
});
|
|
5849
|
+
}
|
|
5850
|
+
/**
|
|
5050
5851
|
* Groups records by hostname, preserving insertion order.
|
|
5051
5852
|
*/
|
|
5052
5853
|
function groupByHostname(records) {
|
|
@@ -5061,8 +5862,8 @@ function groupByHostname(records) {
|
|
|
5061
5862
|
/**
|
|
5062
5863
|
* Builds an HDF baseline from all records for a single host.
|
|
5063
5864
|
*/
|
|
5064
|
-
function buildBaseline(hostname, records, resultsChecksum) {
|
|
5065
|
-
return createMinimalBaseline("Prisma Cloud Scan", limitArrayWithWarning(records, "finding").map((rec) => buildRequirement$4(rec)), {
|
|
5865
|
+
function buildBaseline(hostname, records, resultsChecksum, scanTime) {
|
|
5866
|
+
return createMinimalBaseline("Prisma Cloud Scan", limitArrayWithWarning(records, "finding").map((rec) => buildRequirement$4(rec, scanTime)), {
|
|
5066
5867
|
resultsChecksum,
|
|
5067
5868
|
title: `Prisma Cloud Scan (${hostname})`
|
|
5068
5869
|
});
|
|
@@ -5077,20 +5878,26 @@ function buildBaseline(hostname, records, resultsChecksum) {
|
|
|
5077
5878
|
async function convertPrismaToHdf(input) {
|
|
5078
5879
|
if (!input || input.trim().length === 0) throw new Error("prisma: empty input");
|
|
5079
5880
|
validateInputSize(input, "prisma");
|
|
5881
|
+
const headers = (input.split(/\r?\n/, 1)[0] ?? "").split(",").map((h) => h.trim());
|
|
5882
|
+
for (const col of REQUIRED_COLUMNS) if (!headers.includes(col)) throw new Error(`prisma: missing required CSV column "${col}"`);
|
|
5080
5883
|
const records = parseCsv(input);
|
|
5081
|
-
if (records.length === 0) throw new Error("prisma: no data rows in CSV");
|
|
5082
|
-
const firstRecord = records[0];
|
|
5083
|
-
for (const col of REQUIRED_COLUMNS) if (!(col in firstRecord)) throw new Error(`prisma: missing required CSV column "${col}"`);
|
|
5084
5884
|
const resultsChecksum = await inputChecksum(input);
|
|
5085
|
-
const
|
|
5885
|
+
const scanTime = /* @__PURE__ */ new Date();
|
|
5086
5886
|
const baselines = [];
|
|
5087
5887
|
const components = [];
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5888
|
+
if (records.length === 0) baselines.push(createMinimalBaseline("Prisma Cloud Scan", [buildNoFindingsRequirement("prisma-no-findings", "Prisma Cloud scanned the workload and reported zero vulnerable components.", scanTime)], {
|
|
5889
|
+
resultsChecksum,
|
|
5890
|
+
title: "Prisma Cloud Scan"
|
|
5891
|
+
}));
|
|
5892
|
+
else {
|
|
5893
|
+
const hostGroups = groupByHostname(records);
|
|
5894
|
+
for (const [hostname, hostRecords] of hostGroups) {
|
|
5895
|
+
baselines.push(buildBaseline(hostname, hostRecords, resultsChecksum, scanTime));
|
|
5896
|
+
components.push({
|
|
5897
|
+
name: hostname,
|
|
5898
|
+
type: TargetType.Host
|
|
5899
|
+
});
|
|
5900
|
+
}
|
|
5094
5901
|
}
|
|
5095
5902
|
return buildHdfResults({
|
|
5096
5903
|
generatorName: "prisma-to-hdf",
|
|
@@ -5099,7 +5906,7 @@ async function convertPrismaToHdf(input) {
|
|
|
5099
5906
|
toolFormat: "CSV",
|
|
5100
5907
|
baselines,
|
|
5101
5908
|
components,
|
|
5102
|
-
timestamp:
|
|
5909
|
+
timestamp: scanTime
|
|
5103
5910
|
});
|
|
5104
5911
|
}
|
|
5105
5912
|
//#endregion
|
|
@@ -5115,6 +5922,15 @@ const IMPACT_MAPPING$3 = {
|
|
|
5115
5922
|
function getImpact$2(severity) {
|
|
5116
5923
|
return IMPACT_MAPPING$3[severity.toLowerCase()] ?? .5;
|
|
5117
5924
|
}
|
|
5925
|
+
const NETSPARKER_US_DATETIME = /^\d{2}\/\d{2}\/\d{4} \d{1,2}:\d{2} (AM|PM)$/;
|
|
5926
|
+
function parseNetsparkerTimestamp(s) {
|
|
5927
|
+
const trimmed = s.trim();
|
|
5928
|
+
if (NETSPARKER_US_DATETIME.test(trimmed)) {
|
|
5929
|
+
const d = /* @__PURE__ */ new Date(`${trimmed} GMT`);
|
|
5930
|
+
if (!isNaN(d.getTime())) return d;
|
|
5931
|
+
}
|
|
5932
|
+
return parseTimestamp(s);
|
|
5933
|
+
}
|
|
5118
5934
|
function formatCodeDesc(request) {
|
|
5119
5935
|
const parts = [];
|
|
5120
5936
|
parts.push(`http-request : ${request?.content ?? ""}`);
|
|
@@ -5196,7 +6012,7 @@ function buildRequirement$3(vuln, initiated) {
|
|
|
5196
6012
|
});
|
|
5197
6013
|
const codeDesc = formatCodeDesc(vuln["http-request"]);
|
|
5198
6014
|
const message = formatMessage$1(vuln["http-response"]);
|
|
5199
|
-
const startTime = initiated ?
|
|
6015
|
+
const startTime = (initiated ? parseNetsparkerTimestamp(initiated) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
|
|
5200
6016
|
const results = [{
|
|
5201
6017
|
status: ResultStatus.Failed,
|
|
5202
6018
|
codeDesc,
|
|
@@ -5239,20 +6055,24 @@ async function convertNetsparkerToHdf(input) {
|
|
|
5239
6055
|
const { items: limitedVulns, truncated } = limitArray(vulns);
|
|
5240
6056
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
5241
6057
|
if (truncated) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${vulns.length})`);
|
|
5242
|
-
const baseline = createMinimalBaseline("Netsparker Scan", limitedVulns.map((vuln) => buildRequirement$3(vuln, initiated)), {
|
|
5243
|
-
resultsChecksum,
|
|
5244
|
-
title: `${toolName} Enterprise Scan ID: ${target["scan-id"] ?? ""} URL: ${target.url ?? ""}`
|
|
5245
|
-
});
|
|
5246
6058
|
const targetName = target.url ?? "Unknown";
|
|
6059
|
+
const requirements = limitedVulns.map((vuln) => buildRequirement$3(vuln, initiated));
|
|
6060
|
+
if (requirements.length === 0) {
|
|
6061
|
+
const startTime = (initiated ? parseNetsparkerTimestamp(initiated) : null) ?? /* @__PURE__ */ new Date();
|
|
6062
|
+
requirements.push(buildNoFindingsRequirement("netsparker-no-findings", `${toolName} scanned ${targetName} and reported zero findings.`, startTime));
|
|
6063
|
+
}
|
|
5247
6064
|
return buildHdfResults({
|
|
5248
6065
|
generatorName: "netsparker-to-hdf",
|
|
5249
6066
|
converterVersion: "1.0.0",
|
|
5250
6067
|
toolName,
|
|
5251
6068
|
toolFormat: "XML",
|
|
5252
|
-
baselines: [
|
|
6069
|
+
baselines: [createMinimalBaseline("Netsparker Scan", requirements, {
|
|
6070
|
+
resultsChecksum,
|
|
6071
|
+
title: `${toolName} Enterprise Scan ID: ${target["scan-id"] ?? ""} URL: ${target.url ?? ""}`
|
|
6072
|
+
})],
|
|
5253
6073
|
components: [{
|
|
5254
6074
|
name: targetName,
|
|
5255
|
-
type:
|
|
6075
|
+
type: TargetType.Application
|
|
5256
6076
|
}]
|
|
5257
6077
|
});
|
|
5258
6078
|
}
|
|
@@ -5345,7 +6165,7 @@ function buildRequirement$2(ruleID, finding, startTime) {
|
|
|
5345
6165
|
});
|
|
5346
6166
|
const resultObj = createResult(getStatus$1(finding.checked_items, finding.flagged_items), getMessage(finding.checked_items, finding.flagged_items, finding.items), {
|
|
5347
6167
|
codeDesc: finding.description,
|
|
5348
|
-
startTime: startTime ?
|
|
6168
|
+
startTime: (startTime ? parseTimestamp(startTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
|
|
5349
6169
|
});
|
|
5350
6170
|
const req = createRequirement(ruleID, finding.description, descriptions, getImpact$1(finding.level), [resultObj], { tags });
|
|
5351
6171
|
const controlType = deriveControlTypeFromTags(nist);
|
|
@@ -5367,12 +6187,14 @@ async function convertScoutsuiteToHdf(input) {
|
|
|
5367
6187
|
const resultsChecksum = await inputChecksum(jsonStr);
|
|
5368
6188
|
const report = parseJSON(jsonStr);
|
|
5369
6189
|
if (!report || typeof report !== "object") throw new Error("scoutsuite: invalid JSON");
|
|
5370
|
-
const
|
|
6190
|
+
const requirements = limitArrayWithWarning(collapseFindings(report), "finding").map(([ruleID, finding]) => buildRequirement$2(ruleID, finding, report.last_run.time));
|
|
6191
|
+
const targetName = `${report.last_run.ruleset_name} ruleset:${report.provider_name}:${report.account_id}`;
|
|
6192
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("scoutsuite-no-findings", `ScoutSuite scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
6193
|
+
const baseline = createMinimalBaseline("ScoutSuite Scan", requirements, {
|
|
5371
6194
|
resultsChecksum,
|
|
5372
6195
|
title: `Scout Suite Report using ${report.last_run.ruleset_name} ruleset on ${report.provider_name} with account ${report.account_id}`,
|
|
5373
6196
|
summary: report.last_run.ruleset_about
|
|
5374
6197
|
});
|
|
5375
|
-
const targetName = `${report.last_run.ruleset_name} ruleset:${report.provider_name}:${report.account_id}`;
|
|
5376
6198
|
return buildHdfResults({
|
|
5377
6199
|
generatorName: "scoutsuite-to-hdf",
|
|
5378
6200
|
converterVersion: "1.0.0",
|
|
@@ -5382,13 +6204,13 @@ async function convertScoutsuiteToHdf(input) {
|
|
|
5382
6204
|
baselines: [baseline],
|
|
5383
6205
|
components: [{
|
|
5384
6206
|
name: targetName,
|
|
5385
|
-
type:
|
|
6207
|
+
type: TargetType.CloudAccount,
|
|
5386
6208
|
labels: {
|
|
5387
6209
|
account: report.account_id,
|
|
5388
6210
|
provider: report.provider_code ?? report.provider_name
|
|
5389
6211
|
}
|
|
5390
6212
|
}],
|
|
5391
|
-
timestamp: report.last_run.time ?
|
|
6213
|
+
timestamp: report.last_run.time ? parseTimestamp(report.last_run.time) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date()
|
|
5392
6214
|
});
|
|
5393
6215
|
}
|
|
5394
6216
|
//#endregion
|
|
@@ -5485,7 +6307,7 @@ function buildRequirementFromResult(result, filename) {
|
|
|
5485
6307
|
}];
|
|
5486
6308
|
const scannerName = result.response.service_name;
|
|
5487
6309
|
const startTimeStr = result.response.milestones?.service_started ?? "";
|
|
5488
|
-
const startTime = startTimeStr ?
|
|
6310
|
+
const startTime = (startTimeStr ? parseTimestamp(startTimeStr) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
|
|
5489
6311
|
const score = result.result.score;
|
|
5490
6312
|
const status = determineStatus(score);
|
|
5491
6313
|
let results;
|
|
@@ -5539,6 +6361,10 @@ async function convertConveyorToHdf(input) {
|
|
|
5539
6361
|
const desc = data.api_response.params["description"];
|
|
5540
6362
|
if (typeof desc === "string" && desc.length > 0) targetName = desc;
|
|
5541
6363
|
}
|
|
6364
|
+
if (baselines.length === 0) baselines.push(createMinimalBaseline("Conveyor Scan", [buildNoFindingsRequirement("conveyor-no-findings", `Conveyor scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date())], {
|
|
6365
|
+
resultsChecksum,
|
|
6366
|
+
title: "Conveyor Scan (no findings)"
|
|
6367
|
+
}));
|
|
5542
6368
|
return buildHdfResults({
|
|
5543
6369
|
generatorName: "conveyor-to-hdf",
|
|
5544
6370
|
converterVersion: "1.0.0",
|
|
@@ -5547,7 +6373,7 @@ async function convertConveyorToHdf(input) {
|
|
|
5547
6373
|
baselines,
|
|
5548
6374
|
components: [{
|
|
5549
6375
|
name: targetName,
|
|
5550
|
-
type:
|
|
6376
|
+
type: TargetType.Application
|
|
5551
6377
|
}],
|
|
5552
6378
|
timestamp: /* @__PURE__ */ new Date()
|
|
5553
6379
|
});
|
|
@@ -5569,7 +6395,7 @@ async function convertConveyorToHdf(input) {
|
|
|
5569
6395
|
/** Attribute prefix used by fast-xml-parser — all XML attributes are accessed via `@_name`. */
|
|
5570
6396
|
const A = "@_";
|
|
5571
6397
|
/** Veracode severity level (0-5) to HDF impact mapping. */
|
|
5572
|
-
const IMPACT_MAPPING$2 = new Map([
|
|
6398
|
+
const IMPACT_MAPPING$2 = /* @__PURE__ */ new Map([
|
|
5573
6399
|
["5", .9],
|
|
5574
6400
|
["4", .7],
|
|
5575
6401
|
["3", .5],
|
|
@@ -5591,9 +6417,7 @@ function decodeXmlEntities(s) {
|
|
|
5591
6417
|
/** Parse Veracode timestamp format ("2021-12-29 22:16:36 UTC") to Date. */
|
|
5592
6418
|
function parseVeracodeTimestamp(ts) {
|
|
5593
6419
|
if (!ts) return void 0;
|
|
5594
|
-
|
|
5595
|
-
const d = new Date(decoded.replace(" UTC", "Z").replace(" ", "T"));
|
|
5596
|
-
return isNaN(d.getTime()) ? void 0 : d;
|
|
6420
|
+
return parseTimestamp(decodeXmlEntities(ts).replace(" UTC", "Z").replace(" ", "T")) ?? void 0;
|
|
5597
6421
|
}
|
|
5598
6422
|
/** Format description paragraphs into text. */
|
|
5599
6423
|
function formatDesc(desc) {
|
|
@@ -5814,6 +6638,8 @@ async function convertVeracodeToHdf(input) {
|
|
|
5814
6638
|
const cweRequirements = buildCWERequirements(ensureArray(report.severity), firstBuildDate);
|
|
5815
6639
|
const cveRequirements = buildCVERequirements(report.software_composition_analysis, firstBuildDate);
|
|
5816
6640
|
const allRequirements = [...cweRequirements, ...cveRequirements];
|
|
6641
|
+
const targetName = attr(report, "app_name") || "Veracode Application";
|
|
6642
|
+
if (allRequirements.length === 0) allRequirements.push(buildNoFindingsRequirement("veracode-no-findings", `Veracode scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
5817
6643
|
let title;
|
|
5818
6644
|
const staticAnalysis = report["static-analysis"];
|
|
5819
6645
|
if (staticAnalysis) {
|
|
@@ -5829,7 +6655,6 @@ async function convertVeracodeToHdf(input) {
|
|
|
5829
6655
|
version: attr(report, "policy_version"),
|
|
5830
6656
|
summary: attr(report, "policy_name")
|
|
5831
6657
|
});
|
|
5832
|
-
const targetName = attr(report, "app_name") || "Veracode Application";
|
|
5833
6658
|
const timestamp = parseVeracodeTimestamp(firstBuildDate);
|
|
5834
6659
|
return buildHdfResults({
|
|
5835
6660
|
generatorName: "veracode-to-hdf",
|
|
@@ -5839,7 +6664,7 @@ async function convertVeracodeToHdf(input) {
|
|
|
5839
6664
|
baselines: [baseline],
|
|
5840
6665
|
components: [{
|
|
5841
6666
|
name: targetName,
|
|
5842
|
-
type:
|
|
6667
|
+
type: TargetType.Application
|
|
5843
6668
|
}],
|
|
5844
6669
|
timestamp
|
|
5845
6670
|
});
|
|
@@ -5910,7 +6735,7 @@ function buildRequirement$1(cs, profiles, createdDateTime) {
|
|
|
5910
6735
|
});
|
|
5911
6736
|
}
|
|
5912
6737
|
const codeDesc = cs.implementationStatus || "No implementation status provided";
|
|
5913
|
-
const startTime = createdDateTime ?
|
|
6738
|
+
const startTime = (createdDateTime ? parseTimestamp(createdDateTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
|
|
5914
6739
|
const req = createRequirement(id, title, descriptions, impact, [createResult(status, void 0, {
|
|
5915
6740
|
codeDesc,
|
|
5916
6741
|
...startTime ? { startTime } : {}
|
|
@@ -5957,7 +6782,7 @@ async function convertMsftSecureScoreToHdf(input) {
|
|
|
5957
6782
|
}),
|
|
5958
6783
|
components: [{
|
|
5959
6784
|
name: `Azure Tenant: ${tenantId}`,
|
|
5960
|
-
type:
|
|
6785
|
+
type: TargetType.CloudAccount,
|
|
5961
6786
|
labels: {
|
|
5962
6787
|
account: tenantId,
|
|
5963
6788
|
provider: "azure"
|
|
@@ -5980,6 +6805,7 @@ async function convertMsftDefenderDevopsToHdf(input) {
|
|
|
5980
6805
|
const { components, runEnrichments } = extractEnrichments(raw);
|
|
5981
6806
|
const hdfJson = await convertSarifToHdf(input);
|
|
5982
6807
|
const result = JSON.parse(hdfJson);
|
|
6808
|
+
synthesizeNoFindingsPlaceholders(result);
|
|
5983
6809
|
applyEnrichments(result, components, runEnrichments);
|
|
5984
6810
|
if (result.generator) result.generator.name = "msft-defender-devops-to-hdf";
|
|
5985
6811
|
if (result.tool) result.tool.name = "Microsoft Defender for DevOps";
|
|
@@ -5994,7 +6820,7 @@ function extractEnrichments(raw) {
|
|
|
5994
6820
|
seenRepos.add(vcp.repositoryUri);
|
|
5995
6821
|
const target = {
|
|
5996
6822
|
name: repoNameFromURI(vcp.repositoryUri),
|
|
5997
|
-
type:
|
|
6823
|
+
type: TargetType.Repository,
|
|
5998
6824
|
url: vcp.repositoryUri,
|
|
5999
6825
|
labels: {}
|
|
6000
6826
|
};
|
|
@@ -6044,6 +6870,14 @@ function applyEnrichments(result, components, runEnrichments) {
|
|
|
6044
6870
|
}
|
|
6045
6871
|
}
|
|
6046
6872
|
}
|
|
6873
|
+
function synthesizeNoFindingsPlaceholders(result) {
|
|
6874
|
+
const startTime = result.timestamp ?? /* @__PURE__ */ new Date();
|
|
6875
|
+
for (const baseline of result.baselines ?? []) {
|
|
6876
|
+
if (baseline.requirements && baseline.requirements.length > 0) continue;
|
|
6877
|
+
const tool = baseline.name;
|
|
6878
|
+
baseline.requirements = [buildNoFindingsRequirement(`${tool}-no-findings`, `Microsoft Defender for DevOps scanner "${tool}" ran and reported zero findings.`, startTime)];
|
|
6879
|
+
}
|
|
6880
|
+
}
|
|
6047
6881
|
function repoNameFromURI(uri) {
|
|
6048
6882
|
const parts = uri.split("/");
|
|
6049
6883
|
return parts[parts.length - 1] || uri;
|
|
@@ -6083,7 +6917,7 @@ function extractSubscriptionID(resourcePath) {
|
|
|
6083
6917
|
/**
|
|
6084
6918
|
* Converts a group of assessments sharing an assessment ID into one EvaluatedRequirement.
|
|
6085
6919
|
*/
|
|
6086
|
-
function buildRequirement(assessmentID, assessments) {
|
|
6920
|
+
function buildRequirement(assessmentID, assessments, scanTime) {
|
|
6087
6921
|
const rep = assessments[0];
|
|
6088
6922
|
const meta = rep.properties.metadata;
|
|
6089
6923
|
const impact = IMPACT_MAPPING$1[meta.severity.toLowerCase()] ?? .5;
|
|
@@ -6104,7 +6938,7 @@ function buildRequirement(assessmentID, assessments) {
|
|
|
6104
6938
|
label: "fix",
|
|
6105
6939
|
data: meta.remediationDescription
|
|
6106
6940
|
});
|
|
6107
|
-
const results = assessments.map(buildResultFromAssessment);
|
|
6941
|
+
const results = assessments.map((a) => buildResultFromAssessment(a, scanTime));
|
|
6108
6942
|
const req = createRequirement(assessmentID, rep.properties.displayName, descriptions, impact, results, { tags });
|
|
6109
6943
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
6110
6944
|
return req;
|
|
@@ -6112,10 +6946,13 @@ function buildRequirement(assessmentID, assessments) {
|
|
|
6112
6946
|
/**
|
|
6113
6947
|
* Converts a single assessment into an HDF RequirementResult.
|
|
6114
6948
|
*/
|
|
6115
|
-
function buildResultFromAssessment(a) {
|
|
6949
|
+
function buildResultFromAssessment(a, scanTime) {
|
|
6116
6950
|
const status = mapStatus(a.properties.status.code);
|
|
6117
6951
|
const codeDesc = `Resource: ${a.properties.resourceDetails.id}`;
|
|
6118
|
-
return createResult(status, a.properties.status.description || a.properties.status.cause || void 0, {
|
|
6952
|
+
return createResult(status, a.properties.status.description || a.properties.status.cause || void 0, {
|
|
6953
|
+
codeDesc,
|
|
6954
|
+
startTime: scanTime
|
|
6955
|
+
});
|
|
6119
6956
|
}
|
|
6120
6957
|
/**
|
|
6121
6958
|
* Converts Microsoft Defender for Cloud assessment output to HDF format.
|
|
@@ -6125,6 +6962,7 @@ function buildResultFromAssessment(a) {
|
|
|
6125
6962
|
*/
|
|
6126
6963
|
async function convertMsftDefenderCloudToHdf(input) {
|
|
6127
6964
|
validateInputSize(input, "msft-defender-cloud");
|
|
6965
|
+
const scanTime = /* @__PURE__ */ new Date();
|
|
6128
6966
|
const resultsChecksum = await inputChecksum(input);
|
|
6129
6967
|
const raw = parseJSON(input);
|
|
6130
6968
|
if (!raw || typeof raw !== "object") throw new Error("Invalid Defender for Cloud structure: not a valid JSON object");
|
|
@@ -6139,22 +6977,24 @@ async function convertMsftDefenderCloudToHdf(input) {
|
|
|
6139
6977
|
else groups.set(a.name, [a]);
|
|
6140
6978
|
}
|
|
6141
6979
|
const requirements = [];
|
|
6142
|
-
for (const [assessmentID, assessments] of groups) requirements.push(buildRequirement(assessmentID, assessments));
|
|
6980
|
+
for (const [assessmentID, assessments] of groups) requirements.push(buildRequirement(assessmentID, assessments, scanTime));
|
|
6981
|
+
const subscriptionID = limitedAssessments.length > 0 ? extractSubscriptionID(limitedAssessments[0].id) : "";
|
|
6982
|
+
if (requirements.length === 0) {
|
|
6983
|
+
const targetName = subscriptionID || "Unknown";
|
|
6984
|
+
requirements.push(buildNoFindingsRequirement("msft-defender-cloud-no-findings", `Microsoft Defender for Cloud scanned ${targetName} and reported zero findings.`, scanTime));
|
|
6985
|
+
}
|
|
6143
6986
|
const baseline = createMinimalBaseline("Microsoft Defender for Cloud Assessments", requirements, { resultsChecksum });
|
|
6144
6987
|
const components = [];
|
|
6145
|
-
if (
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
}
|
|
6156
|
-
});
|
|
6157
|
-
}
|
|
6988
|
+
if (subscriptionID) components.push({
|
|
6989
|
+
name: `Azure Subscription ${subscriptionID}`,
|
|
6990
|
+
type: TargetType.CloudAccount,
|
|
6991
|
+
accountId: subscriptionID,
|
|
6992
|
+
provider: "azure",
|
|
6993
|
+
labels: {
|
|
6994
|
+
account: subscriptionID,
|
|
6995
|
+
provider: "azure"
|
|
6996
|
+
}
|
|
6997
|
+
});
|
|
6158
6998
|
return buildHdfResults({
|
|
6159
6999
|
generatorName: "msft-defender-cloud-to-hdf",
|
|
6160
7000
|
converterVersion: "1.0.0",
|
|
@@ -6162,7 +7002,7 @@ async function convertMsftDefenderCloudToHdf(input) {
|
|
|
6162
7002
|
toolFormat: "JSON",
|
|
6163
7003
|
baselines: [baseline],
|
|
6164
7004
|
components,
|
|
6165
|
-
timestamp:
|
|
7005
|
+
timestamp: scanTime
|
|
6166
7006
|
});
|
|
6167
7007
|
}
|
|
6168
7008
|
//#endregion
|
|
@@ -6229,7 +7069,7 @@ function extractDeviceTarget(alert) {
|
|
|
6229
7069
|
for (const ev of alert.evidence) if ((ev["@odata.type"] ?? "").includes("deviceEvidence") && ev.deviceDnsName) {
|
|
6230
7070
|
const target = {
|
|
6231
7071
|
name: ev.deviceDnsName,
|
|
6232
|
-
type:
|
|
7072
|
+
type: TargetType.Host,
|
|
6233
7073
|
labels: { provider: "azure" }
|
|
6234
7074
|
};
|
|
6235
7075
|
if (ev.deviceDnsName) target.fqdn = ev.deviceDnsName;
|
|
@@ -6239,7 +7079,7 @@ function extractDeviceTarget(alert) {
|
|
|
6239
7079
|
}
|
|
6240
7080
|
return {
|
|
6241
7081
|
name: alert.tenantId ?? "unknown",
|
|
6242
|
-
type:
|
|
7082
|
+
type: TargetType.CloudAccount,
|
|
6243
7083
|
accountId: alert.tenantId,
|
|
6244
7084
|
labels: {
|
|
6245
7085
|
account: alert.tenantId ?? "",
|
|
@@ -6259,13 +7099,26 @@ function buildTags$1(alert) {
|
|
|
6259
7099
|
return tags;
|
|
6260
7100
|
}
|
|
6261
7101
|
/**
|
|
7102
|
+
* Resolves a clean per-finding source timestamp, falling back to the conversion time.
|
|
7103
|
+
*/
|
|
7104
|
+
function resolveStartTime(alert, scanTime) {
|
|
7105
|
+
const first = alert.firstActivityDateTime ? parseTimestamp(alert.firstActivityDateTime) : null;
|
|
7106
|
+
if (first) return first;
|
|
7107
|
+
const created = alert.createdDateTime ? parseTimestamp(alert.createdDateTime) : null;
|
|
7108
|
+
if (created) return created;
|
|
7109
|
+
return scanTime;
|
|
7110
|
+
}
|
|
7111
|
+
/**
|
|
6262
7112
|
* Converts a single MDE alert to an HDF EvaluatedRequirement.
|
|
6263
7113
|
*/
|
|
6264
|
-
function alertToRequirement(alert) {
|
|
7114
|
+
function alertToRequirement(alert, scanTime) {
|
|
6265
7115
|
const impact = severityToImpact$1(alert.severity);
|
|
6266
7116
|
const status = statusToResultStatus(alert.status, alert.classification);
|
|
6267
7117
|
const codeDesc = formatEvidence(alert.evidence ?? []);
|
|
6268
|
-
const results = [createResult(status, formatMessage(alert), {
|
|
7118
|
+
const results = [createResult(status, formatMessage(alert), {
|
|
7119
|
+
codeDesc,
|
|
7120
|
+
startTime: resolveStartTime(alert, scanTime)
|
|
7121
|
+
})];
|
|
6269
7122
|
const descriptions = [{
|
|
6270
7123
|
label: "default",
|
|
6271
7124
|
data: alert.description
|
|
@@ -6295,10 +7148,11 @@ async function convertMsftDefenderEndpointToHdf(input) {
|
|
|
6295
7148
|
const response = parseJSON(input);
|
|
6296
7149
|
if (!response || typeof response !== "object") throw new Error("Invalid Microsoft Defender for Endpoint structure: not a valid JSON object");
|
|
6297
7150
|
if (!Array.isArray(response.value)) throw new Error("Invalid Microsoft Defender for Endpoint structure: missing or invalid value array");
|
|
7151
|
+
const scanTime = /* @__PURE__ */ new Date();
|
|
6298
7152
|
const { items: limitedAlerts, truncated } = limitArray(response.value);
|
|
6299
7153
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
6300
7154
|
if (truncated) console.warn(`WARNING: Input truncated at ${limitedAlerts.length} alert items (original: ${response.value.length})`);
|
|
6301
|
-
const requirements = limitedAlerts.map(alertToRequirement);
|
|
7155
|
+
const requirements = limitedAlerts.map((alert) => alertToRequirement(alert, scanTime));
|
|
6302
7156
|
const seenTargets = /* @__PURE__ */ new Set();
|
|
6303
7157
|
const components = [];
|
|
6304
7158
|
for (const alert of limitedAlerts) {
|
|
@@ -6308,13 +7162,14 @@ async function convertMsftDefenderEndpointToHdf(input) {
|
|
|
6308
7162
|
components.push(target);
|
|
6309
7163
|
}
|
|
6310
7164
|
}
|
|
7165
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("msft-defender-endpoint-no-findings", "Microsoft Defender for Endpoint scanned the tenant and reported zero findings.", scanTime));
|
|
6311
7166
|
return buildHdfResults({
|
|
6312
7167
|
generatorName: "msft-defender-endpoint-to-hdf",
|
|
6313
7168
|
converterVersion: "1.0.0",
|
|
6314
7169
|
toolName: "Microsoft Defender for Endpoint",
|
|
6315
7170
|
baselines: [createMinimalBaseline("Microsoft Defender for Endpoint Scan", requirements, { resultsChecksum })],
|
|
6316
7171
|
components,
|
|
6317
|
-
timestamp:
|
|
7172
|
+
timestamp: scanTime
|
|
6318
7173
|
});
|
|
6319
7174
|
}
|
|
6320
7175
|
//#endregion
|
|
@@ -6348,7 +7203,7 @@ function wrap(value) {
|
|
|
6348
7203
|
return { "#text": value };
|
|
6349
7204
|
}
|
|
6350
7205
|
/** Map HDF impact (0.0-1.0) to XCCDF severity string. */
|
|
6351
|
-
function impactToSeverity$
|
|
7206
|
+
function impactToSeverity$2(impact) {
|
|
6352
7207
|
if (impact >= .7) return "high";
|
|
6353
7208
|
if (impact >= .4) return "medium";
|
|
6354
7209
|
if (impact >= .1) return "low";
|
|
@@ -6402,7 +7257,7 @@ function buildRuleObj(req) {
|
|
|
6402
7257
|
const ruleId = sanitizeXccdfId("xccdf_hdf_rule_" + req.id + "_rule");
|
|
6403
7258
|
const rule = {
|
|
6404
7259
|
[`${ATTR}id`]: ruleId,
|
|
6405
|
-
[`${ATTR}severity`]: impactToSeverity$
|
|
7260
|
+
[`${ATTR}severity`]: impactToSeverity$2(req.impact),
|
|
6406
7261
|
[`${ATTR}selected`]: "true",
|
|
6407
7262
|
title: wrap(req.title ?? req.id)
|
|
6408
7263
|
};
|
|
@@ -6448,8 +7303,7 @@ function buildTestResultObj(hdfData, baseline) {
|
|
|
6448
7303
|
[`${ATTR}idref`]: ruleIdRef,
|
|
6449
7304
|
result: wrap(hdfStatusToXccdf(result.status))
|
|
6450
7305
|
};
|
|
6451
|
-
|
|
6452
|
-
rr[`${ATTR}time`] = startTime;
|
|
7306
|
+
rr[`${ATTR}time`] = typeof result.startTime === "string" ? result.startTime : result.startTime.toISOString();
|
|
6453
7307
|
if (result.message) rr.message = wrap(result.message);
|
|
6454
7308
|
if (result.codeDesc) rr.check = {
|
|
6455
7309
|
[`${ATTR}system`]: "http://oval.mitre.org/XMLSchema/oval-definitions-5",
|
|
@@ -6593,7 +7447,7 @@ function nistTagToControlId(tag) {
|
|
|
6593
7447
|
* Converts a 0.0-1.0 impact value to an OSCAL severity string.
|
|
6594
7448
|
* This is the reverse of extractRiskSeverity.
|
|
6595
7449
|
*/
|
|
6596
|
-
function impactToSeverity(impact) {
|
|
7450
|
+
function impactToSeverity$1(impact) {
|
|
6597
7451
|
if (impact >= .9) return "critical";
|
|
6598
7452
|
if (impact >= .7) return "high";
|
|
6599
7453
|
if (impact >= .4) return "moderate";
|
|
@@ -6748,7 +7602,7 @@ function requirementToFindingSet(req, timestamp) {
|
|
|
6748
7602
|
let risk;
|
|
6749
7603
|
if (req.impact > 0) {
|
|
6750
7604
|
const riskUUID = crypto.randomUUID();
|
|
6751
|
-
const severity = impactToSeverity(req.impact);
|
|
7605
|
+
const severity = impactToSeverity$1(req.impact);
|
|
6752
7606
|
risk = {
|
|
6753
7607
|
uuid: riskUUID,
|
|
6754
7608
|
title: `Risk for ${req.id}`,
|
|
@@ -6879,7 +7733,7 @@ async function convertHdfToOscalPoam(input) {
|
|
|
6879
7733
|
return JSON.stringify(doc, null, 2);
|
|
6880
7734
|
}
|
|
6881
7735
|
/**
|
|
6882
|
-
* Converts parsed
|
|
7736
|
+
* Converts parsed HDFAmendments to an OSCAL PlanOfActionAndMilestones.
|
|
6883
7737
|
*/
|
|
6884
7738
|
function amendmentsToPOAM(amendments) {
|
|
6885
7739
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -7079,6 +7933,1150 @@ async function convertIonchannelToHdf(input) {
|
|
|
7079
7933
|
});
|
|
7080
7934
|
}
|
|
7081
7935
|
//#endregion
|
|
7936
|
+
//#region shared/typescript/vex/mapping.ts
|
|
7937
|
+
/** Canonical VEX status across OpenVEX / CSAF / CycloneDX. */
|
|
7938
|
+
const VexStatus = {
|
|
7939
|
+
NotAffected: "not_affected",
|
|
7940
|
+
Affected: "affected",
|
|
7941
|
+
Fixed: "fixed",
|
|
7942
|
+
UnderInvestigation: "under_investigation"
|
|
7943
|
+
};
|
|
7944
|
+
/**
|
|
7945
|
+
* Map an ecosystem-specific status string to the canonical VexStatus.
|
|
7946
|
+
* Returns undefined for values without a clean mapping — caller should warn
|
|
7947
|
+
* and skip, not guess.
|
|
7948
|
+
*/
|
|
7949
|
+
function normalizeStatus(raw) {
|
|
7950
|
+
switch (raw.trim().toLowerCase()) {
|
|
7951
|
+
case "not_affected":
|
|
7952
|
+
case "known_not_affected":
|
|
7953
|
+
case "false_positive": return VexStatus.NotAffected;
|
|
7954
|
+
case "affected":
|
|
7955
|
+
case "known_affected":
|
|
7956
|
+
case "exploitable": return VexStatus.Affected;
|
|
7957
|
+
case "fixed":
|
|
7958
|
+
case "first_fixed":
|
|
7959
|
+
case "resolved":
|
|
7960
|
+
case "resolved_with_pedigree": return VexStatus.Fixed;
|
|
7961
|
+
case "under_investigation":
|
|
7962
|
+
case "in_triage": return VexStatus.UnderInvestigation;
|
|
7963
|
+
default: return;
|
|
7964
|
+
}
|
|
7965
|
+
}
|
|
7966
|
+
/**
|
|
7967
|
+
* Map an ecosystem-specific justification string to the canonical HDF
|
|
7968
|
+
* Justification enum. Returns undefined for unknown values; callers SHOULD
|
|
7969
|
+
* log unknown values rather than silently dropping (the schema spec wants
|
|
7970
|
+
* pass-through, but practically we expect the enum to be extended when a
|
|
7971
|
+
* new vocabulary is integrated rather than carrying raw labels
|
|
7972
|
+
* indefinitely).
|
|
7973
|
+
*
|
|
7974
|
+
* The HDF Justification enum (v3.2.x) covers:
|
|
7975
|
+
* - the original OpenVEX / CSAF VEX five values
|
|
7976
|
+
* - CycloneDX-specific reachability values (requires_*, protected_*)
|
|
7977
|
+
* that describe why a vulnerable code path is unreachable in the
|
|
7978
|
+
* deployed configuration.
|
|
7979
|
+
*/
|
|
7980
|
+
function normalizeJustification(raw) {
|
|
7981
|
+
switch (raw.trim().toLowerCase()) {
|
|
7982
|
+
case "component_not_present":
|
|
7983
|
+
case "code_not_present": return Justification.ComponentNotPresent;
|
|
7984
|
+
case "vulnerable_code_not_present": return Justification.VulnerableCodeNotPresent;
|
|
7985
|
+
case "vulnerable_code_not_in_execute_path":
|
|
7986
|
+
case "code_not_reachable": return Justification.VulnerableCodeNotInExecutePath;
|
|
7987
|
+
case "vulnerable_code_cannot_be_controlled_by_adversary": return Justification.VulnerableCodeCannotBeControlledByAdversary;
|
|
7988
|
+
case "inline_mitigations_already_exist":
|
|
7989
|
+
case "protected_by_mitigating_control": return Justification.InlineMitigationsAlreadyExist;
|
|
7990
|
+
case "requires_configuration": return Justification.RequiresConfiguration;
|
|
7991
|
+
case "requires_dependency": return Justification.RequiresDependency;
|
|
7992
|
+
case "requires_environment": return Justification.RequiresEnvironment;
|
|
7993
|
+
case "protected_by_compiler": return Justification.ProtectedByCompiler;
|
|
7994
|
+
case "protected_at_runtime": return Justification.ProtectedAtRuntime;
|
|
7995
|
+
case "protected_at_perimeter": return Justification.ProtectedAtPerimeter;
|
|
7996
|
+
default: return;
|
|
7997
|
+
}
|
|
7998
|
+
}
|
|
7999
|
+
/**
|
|
8000
|
+
* Render an HDF Justification value as the CycloneDX-native vocabulary.
|
|
8001
|
+
* CycloneDX uses short-form names (code_not_present, code_not_reachable,
|
|
8002
|
+
* protected_by_mitigating_control) for the three justifications shared
|
|
8003
|
+
* with OpenVEX/CSAF, and shares the six CycloneDX-specific reachability
|
|
8004
|
+
* values verbatim.
|
|
8005
|
+
*
|
|
8006
|
+
* Returns undefined when the HDF value has no equivalent in CycloneDX's
|
|
8007
|
+
* enum (vulnerable_code_not_present and
|
|
8008
|
+
* vulnerable_code_cannot_be_controlled_by_adversary). Callers should
|
|
8009
|
+
* omit the justification field in that case.
|
|
8010
|
+
*/
|
|
8011
|
+
function justificationForCycloneDX(j) {
|
|
8012
|
+
switch (j) {
|
|
8013
|
+
case Justification.ComponentNotPresent: return "code_not_present";
|
|
8014
|
+
case Justification.VulnerableCodeNotInExecutePath: return "code_not_reachable";
|
|
8015
|
+
case Justification.InlineMitigationsAlreadyExist: return "protected_by_mitigating_control";
|
|
8016
|
+
case Justification.RequiresConfiguration:
|
|
8017
|
+
case Justification.RequiresDependency:
|
|
8018
|
+
case Justification.RequiresEnvironment:
|
|
8019
|
+
case Justification.ProtectedByCompiler:
|
|
8020
|
+
case Justification.ProtectedAtRuntime:
|
|
8021
|
+
case Justification.ProtectedAtPerimeter: return String(j);
|
|
8022
|
+
default: return;
|
|
8023
|
+
}
|
|
8024
|
+
}
|
|
8025
|
+
/**
|
|
8026
|
+
* Return the amendment shape an importer should produce for a canonical VEX
|
|
8027
|
+
* status. Returns undefined for "affected" and "under_investigation" — those
|
|
8028
|
+
* are informational; the consumer creates an amendment later if they act.
|
|
8029
|
+
*/
|
|
8030
|
+
function importTargetFor(status) {
|
|
8031
|
+
switch (status) {
|
|
8032
|
+
case VexStatus.NotAffected: return {
|
|
8033
|
+
overrideType: OverrideType.FalsePositive,
|
|
8034
|
+
status: ResultStatus.Passed,
|
|
8035
|
+
setJustification: true,
|
|
8036
|
+
poamActionTemplate: ""
|
|
8037
|
+
};
|
|
8038
|
+
case VexStatus.Fixed: return {
|
|
8039
|
+
overrideType: OverrideType.Poam,
|
|
8040
|
+
status: ResultStatus.Failed,
|
|
8041
|
+
setJustification: false,
|
|
8042
|
+
poamActionTemplate: "vendor reports fix; apply and re-scan to verify"
|
|
8043
|
+
};
|
|
8044
|
+
case VexStatus.Affected:
|
|
8045
|
+
case VexStatus.UnderInvestigation: return;
|
|
8046
|
+
/* c8 ignore next 2 — every VexStatus has a case above */
|
|
8047
|
+
default: return;
|
|
8048
|
+
}
|
|
8049
|
+
}
|
|
8050
|
+
/**
|
|
8051
|
+
* Return the canonical VEX status an exporter should emit for an HDF
|
|
8052
|
+
* override. Returns undefined when no VEX statement should be emitted —
|
|
8053
|
+
* the consumer has not acted, and VEX requires a deliberate statement.
|
|
8054
|
+
*
|
|
8055
|
+
* `allMilestonesCompleted` and `closureChained` are consulted only for
|
|
8056
|
+
* POA&M overrides. Closure is signalled by BOTH flags being true; either
|
|
8057
|
+
* alone is insufficient.
|
|
8058
|
+
*/
|
|
8059
|
+
function exportStatusFor(override, allMilestonesCompleted, closureChained) {
|
|
8060
|
+
if (!override) return;
|
|
8061
|
+
if (override.justification) return VexStatus.NotAffected;
|
|
8062
|
+
switch (override.type) {
|
|
8063
|
+
case OverrideType.FalsePositive:
|
|
8064
|
+
case OverrideType.Attestation:
|
|
8065
|
+
case OverrideType.Inherited: return VexStatus.NotAffected;
|
|
8066
|
+
case OverrideType.Waiver:
|
|
8067
|
+
case OverrideType.RiskAdjustment:
|
|
8068
|
+
case OverrideType.OperationalRequirement: return VexStatus.Affected;
|
|
8069
|
+
case OverrideType.Poam: return allMilestonesCompleted && closureChained ? VexStatus.Fixed : VexStatus.Affected;
|
|
8070
|
+
/* c8 ignore next 2 — every OverrideType has a case above */
|
|
8071
|
+
default: return;
|
|
8072
|
+
}
|
|
8073
|
+
}
|
|
8074
|
+
const ECOSYSTEM_FROM_PURL_TYPE = {
|
|
8075
|
+
npm: Ecosystem.Npm,
|
|
8076
|
+
pypi: Ecosystem.Pypi,
|
|
8077
|
+
rpm: Ecosystem.RPM,
|
|
8078
|
+
deb: Ecosystem.Deb,
|
|
8079
|
+
maven: Ecosystem.Maven,
|
|
8080
|
+
gem: Ecosystem.Gem,
|
|
8081
|
+
nuget: Ecosystem.Nuget,
|
|
8082
|
+
golang: Ecosystem.Go,
|
|
8083
|
+
go: Ecosystem.Go,
|
|
8084
|
+
cargo: Ecosystem.Cargo
|
|
8085
|
+
};
|
|
8086
|
+
/**
|
|
8087
|
+
* Build an AffectedPackage from a single product identifier string emitted
|
|
8088
|
+
* by a VEX format. Recognizes PURLs and CPE 2.3 strings; returns undefined
|
|
8089
|
+
* for opaque identifiers (importer should drop the entry — schema additions
|
|
8090
|
+
* forbid fabricating name+version).
|
|
8091
|
+
*/
|
|
8092
|
+
function affectedPackageFromIdentifier(identifier) {
|
|
8093
|
+
if (!identifier) return void 0;
|
|
8094
|
+
if (identifier.startsWith("pkg:")) {
|
|
8095
|
+
const parsed = parsePurl(identifier);
|
|
8096
|
+
if (parsed) {
|
|
8097
|
+
const pkg = { purl: identifier };
|
|
8098
|
+
/* c8 ignore next 2 — parsePurl populates name/version when the
|
|
8099
|
+
identifier was a well-formed purl; the absent branches require a
|
|
8100
|
+
malformed-but-prefixed input that loses these fields without
|
|
8101
|
+
triggering the outer null return. Defensive. */
|
|
8102
|
+
if (parsed.name) pkg.name = parsed.name;
|
|
8103
|
+
if (parsed.version) pkg.version = parsed.version;
|
|
8104
|
+
pkg.ecosystem = ECOSYSTEM_FROM_PURL_TYPE[parsed.type] ?? Ecosystem.Generic;
|
|
8105
|
+
return pkg;
|
|
8106
|
+
}
|
|
8107
|
+
return { purl: identifier };
|
|
8108
|
+
}
|
|
8109
|
+
if (identifier.startsWith("cpe:2.3:")) return { cpe: identifier };
|
|
8110
|
+
}
|
|
8111
|
+
/**
|
|
8112
|
+
* Build a unique list of AffectedPackage entries from a sequence of product
|
|
8113
|
+
* identifier strings. Empty / unresolvable identifiers are dropped; duplicate
|
|
8114
|
+
* purls/cpes/names are collapsed.
|
|
8115
|
+
*/
|
|
8116
|
+
function affectedPackagesFromIdentifiers(identifiers) {
|
|
8117
|
+
const out = [];
|
|
8118
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8119
|
+
for (const id of identifiers) {
|
|
8120
|
+
const pkg = affectedPackageFromIdentifier(id);
|
|
8121
|
+
if (!pkg) continue;
|
|
8122
|
+
/* c8 ignore next — affectedPackageFromIdentifier always emits either
|
|
8123
|
+
purl or cpe, so the name fallback branch isn't reachable through
|
|
8124
|
+
this caller. Kept for safety if a hand-built AffectedPackage flows
|
|
8125
|
+
through a future caller. */
|
|
8126
|
+
const key = pkg.purl ?? pkg.cpe ?? `${pkg.name ?? ""}@${pkg.version ?? ""}`;
|
|
8127
|
+
if (seen.has(key)) continue;
|
|
8128
|
+
seen.add(key);
|
|
8129
|
+
out.push(pkg);
|
|
8130
|
+
}
|
|
8131
|
+
return out;
|
|
8132
|
+
}
|
|
8133
|
+
/**
|
|
8134
|
+
* Render an AffectedPackage as a single identifier string suitable for
|
|
8135
|
+
* round-tripping into a VEX format. Prefers purl > cpe > name@version.
|
|
8136
|
+
* Returns undefined when nothing identifying is set.
|
|
8137
|
+
*/
|
|
8138
|
+
function affectedPackageToIdentifier(pkg) {
|
|
8139
|
+
if (pkg.purl) return pkg.purl;
|
|
8140
|
+
if (pkg.cpe) return pkg.cpe;
|
|
8141
|
+
if (pkg.name && pkg.version) return `${pkg.name}@${pkg.version}`;
|
|
8142
|
+
if (pkg.name) return pkg.name;
|
|
8143
|
+
}
|
|
8144
|
+
/**
|
|
8145
|
+
* Build an HDF Evidence entry pointing at the upstream VEX document. Used by
|
|
8146
|
+
* importers to preserve provenance even though we lose the structured
|
|
8147
|
+
* statement_id. Returns undefined when sourceURI is empty — don't fabricate.
|
|
8148
|
+
*/
|
|
8149
|
+
function supplierEvidence(sourceURI, description) {
|
|
8150
|
+
if (!sourceURI.trim()) return;
|
|
8151
|
+
return {
|
|
8152
|
+
type: EvidenceType.URL,
|
|
8153
|
+
data: sourceURI,
|
|
8154
|
+
description: description?.trim() ? description : "Upstream VEX statement"
|
|
8155
|
+
};
|
|
8156
|
+
}
|
|
8157
|
+
//#endregion
|
|
8158
|
+
//#region converters/openvex-to-hdf/typescript/converter.ts
|
|
8159
|
+
/**
|
|
8160
|
+
* OpenVEX to HDF Amendments converter.
|
|
8161
|
+
*
|
|
8162
|
+
* VEX statements are consumer-attached context for CVE findings. The act of
|
|
8163
|
+
* attaching IS the amendment, so this converter emits HDF Amendments rather
|
|
8164
|
+
* than HDF Results.
|
|
8165
|
+
*
|
|
8166
|
+
* VEX 'fixed' is an abstract supplier claim; the assessed system has not
|
|
8167
|
+
* been verified to carry the fix. Imports therefore become open POA&M
|
|
8168
|
+
* overrides (status pinned to failed pending re-scan), not status flips.
|
|
8169
|
+
*
|
|
8170
|
+
* Spec: https://github.com/openvex/spec
|
|
8171
|
+
*/
|
|
8172
|
+
/** One year in milliseconds. VEX statements are re-evaluated as new info
|
|
8173
|
+
* arrives; one year is a defensive default consistent with the
|
|
8174
|
+
* no-permanent-amendment rule on Standalone_Override. */
|
|
8175
|
+
const DEFAULT_EXPIRY_HORIZON_MS$2 = 365 * 24 * 60 * 60 * 1e3;
|
|
8176
|
+
async function convertOpenVexToHdf(input, converterVersion) {
|
|
8177
|
+
validateInputSize(input, "openvex-to-hdf");
|
|
8178
|
+
const doc = parseJSON(input);
|
|
8179
|
+
const docTime = (doc.timestamp ? parseTimestamp(doc.timestamp) : null) ?? /* @__PURE__ */ new Date();
|
|
8180
|
+
const overrides = [];
|
|
8181
|
+
for (const stmt of doc.statements ?? []) {
|
|
8182
|
+
const override = statementToOverride(stmt, doc, docTime);
|
|
8183
|
+
if (override) overrides.push(override);
|
|
8184
|
+
}
|
|
8185
|
+
if (overrides.length === 0) throw new Error("openvex-to-hdf: VEX document contains no actionable statements (all 'affected' or 'under_investigation'); no amendment to write");
|
|
8186
|
+
return {
|
|
8187
|
+
name: doc.author ? `OpenVEX statements from ${doc.author}` : "OpenVEX statements",
|
|
8188
|
+
description: `Imported VEX statements from ${truncateId(doc["@id"] ?? "")}`,
|
|
8189
|
+
overrides,
|
|
8190
|
+
appliedBy: identityFor$1(doc.author, doc.role),
|
|
8191
|
+
generator: {
|
|
8192
|
+
name: "openvex-to-hdf",
|
|
8193
|
+
version: converterVersion
|
|
8194
|
+
},
|
|
8195
|
+
integrity: await inputIntegrity(input)
|
|
8196
|
+
};
|
|
8197
|
+
}
|
|
8198
|
+
function statementToOverride(stmt, doc, docTime) {
|
|
8199
|
+
const canonical = normalizeStatus(stmt.status ?? "");
|
|
8200
|
+
if (!canonical) return void 0;
|
|
8201
|
+
const target = importTargetFor(canonical);
|
|
8202
|
+
if (!target) return void 0;
|
|
8203
|
+
const requirementId = stmt.vulnerability?.name ?? stmt.vulnerability?.["@id"] ?? "";
|
|
8204
|
+
if (!requirementId) return void 0;
|
|
8205
|
+
const stmtTime = (stmt.timestamp ? parseTimestamp(stmt.timestamp) : null) ?? docTime;
|
|
8206
|
+
const author = stmt.author ?? doc.author ?? "";
|
|
8207
|
+
const expiresAt = new Date(stmtTime.getTime() + DEFAULT_EXPIRY_HORIZON_MS$2);
|
|
8208
|
+
const override = {
|
|
8209
|
+
type: target.overrideType,
|
|
8210
|
+
requirementId,
|
|
8211
|
+
appliedAt: stmtTime,
|
|
8212
|
+
expiresAt,
|
|
8213
|
+
appliedBy: identityFor$1(author, doc.role),
|
|
8214
|
+
reason: buildReason$2(stmt, target.poamActionTemplate)
|
|
8215
|
+
};
|
|
8216
|
+
const affectedPackages = affectedPackagesFromIdentifiers((stmt.products ?? []).map((p) => p["@id"]).filter((id) => Boolean(id)));
|
|
8217
|
+
if (affectedPackages.length > 0) override.affectedPackages = affectedPackages;
|
|
8218
|
+
if (target.status !== void 0) override.status = target.status;
|
|
8219
|
+
if (target.setJustification && stmt.justification) {
|
|
8220
|
+
const j = normalizeJustification(stmt.justification);
|
|
8221
|
+
if (j) override.justification = j;
|
|
8222
|
+
}
|
|
8223
|
+
const ev = supplierEvidence(doc["@id"] ?? "", "OpenVEX document");
|
|
8224
|
+
if (ev) override.evidence = [ev];
|
|
8225
|
+
if (target.overrideType === OverrideType.Poam) override.milestones = [{
|
|
8226
|
+
description: target.poamActionTemplate,
|
|
8227
|
+
status: MilestoneStatus.Pending,
|
|
8228
|
+
estimatedCompletion: expiresAt
|
|
8229
|
+
}];
|
|
8230
|
+
return override;
|
|
8231
|
+
}
|
|
8232
|
+
function buildReason$2(stmt, poamTemplate) {
|
|
8233
|
+
const parts = [];
|
|
8234
|
+
if (stmt.impact_statement) parts.push(stmt.impact_statement);
|
|
8235
|
+
if (stmt.action_statement) parts.push(stmt.action_statement);
|
|
8236
|
+
if (parts.length === 0) return poamTemplate || `Imported from OpenVEX status "${stmt.status ?? ""}"`;
|
|
8237
|
+
return parts.join("\n");
|
|
8238
|
+
}
|
|
8239
|
+
function identityFor$1(author, role) {
|
|
8240
|
+
if (!author) return {
|
|
8241
|
+
type: IdentityType.System,
|
|
8242
|
+
identifier: "openvex-import"
|
|
8243
|
+
};
|
|
8244
|
+
const id = {
|
|
8245
|
+
type: author.includes("@") ? IdentityType.Email : IdentityType.Simple,
|
|
8246
|
+
identifier: author
|
|
8247
|
+
};
|
|
8248
|
+
if (role) id.description = role;
|
|
8249
|
+
return id;
|
|
8250
|
+
}
|
|
8251
|
+
function truncateId(id) {
|
|
8252
|
+
const MAX = 80;
|
|
8253
|
+
return id.length <= MAX ? id : `${id.slice(0, MAX)}...`;
|
|
8254
|
+
}
|
|
8255
|
+
//#endregion
|
|
8256
|
+
//#region converters/csaf-vex-to-hdf/typescript/converter.ts
|
|
8257
|
+
/**
|
|
8258
|
+
* CSAF VEX to HDF Amendments converter.
|
|
8259
|
+
*
|
|
8260
|
+
* CSAF (OASIS Common Security Advisory Framework) VEX profile is the
|
|
8261
|
+
* vendor-advisory format. Per the amendment-output pattern (Step 4f),
|
|
8262
|
+
* 'fixed' becomes an open POA&M, not a status flip — supplier claim is
|
|
8263
|
+
* not assessed-system evidence.
|
|
8264
|
+
*
|
|
8265
|
+
* Spec: https://docs.oasis-open.org/csaf/csaf/v2.0/csaf-v2.0.html
|
|
8266
|
+
*/
|
|
8267
|
+
const DEFAULT_EXPIRY_HORIZON_MS$1 = 365 * 24 * 60 * 60 * 1e3;
|
|
8268
|
+
async function convertCsafVexToHdf(input, converterVersion) {
|
|
8269
|
+
validateInputSize(input, "csaf-vex-to-hdf");
|
|
8270
|
+
const doc = parseJSON(input);
|
|
8271
|
+
if (doc.document?.category !== "csaf_vex") throw new Error(`csaf-vex-to-hdf: document.category is ${JSON.stringify(doc.document?.category)}; only 'csaf_vex' is supported`);
|
|
8272
|
+
const dm = doc.document;
|
|
8273
|
+
const tracking = dm.tracking ?? {};
|
|
8274
|
+
const publisher = dm.publisher ?? {};
|
|
8275
|
+
const docTime = (tracking.current_release_date ? parseTimestamp(tracking.current_release_date) : null) ?? /* @__PURE__ */ new Date();
|
|
8276
|
+
const productLookup = buildProductLookup$1(doc.product_tree);
|
|
8277
|
+
const overrides = [];
|
|
8278
|
+
for (const v of doc.vulnerabilities ?? []) overrides.push(...vulnerabilityToOverrides(v, publisher, tracking, docTime, productLookup));
|
|
8279
|
+
if (overrides.length === 0) throw new Error("csaf-vex-to-hdf: CSAF VEX document contains no actionable statements (only affected/under_investigation/recommended); no amendment to write");
|
|
8280
|
+
const publisherName = publisher.name ?? "";
|
|
8281
|
+
return {
|
|
8282
|
+
name: publisherName ? `CSAF VEX statements from ${publisherName}` : "CSAF VEX statements",
|
|
8283
|
+
description: `Imported VEX advisory ${tracking.id ?? ""}`,
|
|
8284
|
+
overrides,
|
|
8285
|
+
appliedBy: identityFor(publisher),
|
|
8286
|
+
generator: {
|
|
8287
|
+
name: "csaf-vex-to-hdf",
|
|
8288
|
+
version: converterVersion
|
|
8289
|
+
},
|
|
8290
|
+
integrity: await inputIntegrity(input),
|
|
8291
|
+
version: tracking.version
|
|
8292
|
+
};
|
|
8293
|
+
}
|
|
8294
|
+
function vulnerabilityToOverrides(vuln, publisher, tracking, docTime, productLookup) {
|
|
8295
|
+
if (!vuln.cve) return [];
|
|
8296
|
+
const ps = vuln.product_status;
|
|
8297
|
+
if (!ps) return [];
|
|
8298
|
+
const out = [];
|
|
8299
|
+
if (ps.known_not_affected?.length) {
|
|
8300
|
+
const o = buildOverride(vuln, publisher, tracking, docTime, VexStatus.NotAffected, ps.known_not_affected, productLookup);
|
|
8301
|
+
if (o) out.push(o);
|
|
8302
|
+
}
|
|
8303
|
+
const fixedProducts = [...ps.fixed ?? [], ...ps.first_fixed ?? []];
|
|
8304
|
+
if (fixedProducts.length > 0) {
|
|
8305
|
+
const o = buildOverride(vuln, publisher, tracking, docTime, VexStatus.Fixed, fixedProducts, productLookup);
|
|
8306
|
+
if (o) out.push(o);
|
|
8307
|
+
}
|
|
8308
|
+
return out;
|
|
8309
|
+
}
|
|
8310
|
+
function buildOverride(vuln, publisher, tracking, docTime, canonical, products, productLookup) {
|
|
8311
|
+
const target = importTargetFor(canonical);
|
|
8312
|
+
if (!target) return void 0;
|
|
8313
|
+
const expiresAt = new Date(docTime.getTime() + DEFAULT_EXPIRY_HORIZON_MS$1);
|
|
8314
|
+
const override = {
|
|
8315
|
+
type: target.overrideType,
|
|
8316
|
+
requirementId: vuln.cve,
|
|
8317
|
+
appliedAt: docTime,
|
|
8318
|
+
expiresAt,
|
|
8319
|
+
appliedBy: identityFor(publisher),
|
|
8320
|
+
reason: buildReason$1(vuln, products)
|
|
8321
|
+
};
|
|
8322
|
+
const affectedPackages = resolveAffectedPackages(products, productLookup);
|
|
8323
|
+
if (affectedPackages.length > 0) override.affectedPackages = affectedPackages;
|
|
8324
|
+
if (target.status !== void 0) override.status = target.status;
|
|
8325
|
+
if (target.setJustification) {
|
|
8326
|
+
const j = pickJustification(vuln, products);
|
|
8327
|
+
if (j) override.justification = j;
|
|
8328
|
+
}
|
|
8329
|
+
const evidence = [];
|
|
8330
|
+
const advisoryEv = supplierEvidence(advisoryURI(publisher, tracking), "CSAF VEX advisory");
|
|
8331
|
+
if (advisoryEv) evidence.push(advisoryEv);
|
|
8332
|
+
for (const r of vuln.references ?? []) {
|
|
8333
|
+
if (!r.url) continue;
|
|
8334
|
+
const ev = supplierEvidence(r.url, r.summary ?? r.category ?? "");
|
|
8335
|
+
if (ev) evidence.push(ev);
|
|
8336
|
+
}
|
|
8337
|
+
if (evidence.length > 0) override.evidence = evidence;
|
|
8338
|
+
if (target.overrideType === OverrideType.Poam) override.milestones = [{
|
|
8339
|
+
description: firstActionRemediation(vuln, products) || target.poamActionTemplate,
|
|
8340
|
+
status: MilestoneStatus.Pending,
|
|
8341
|
+
estimatedCompletion: expiresAt
|
|
8342
|
+
}];
|
|
8343
|
+
return override;
|
|
8344
|
+
}
|
|
8345
|
+
function pickJustification(vuln, products) {
|
|
8346
|
+
const scope = new Set(products);
|
|
8347
|
+
for (const f of vuln.flags ?? []) {
|
|
8348
|
+
if (!overlap(f.product_ids ?? [], scope)) continue;
|
|
8349
|
+
if (!f.label) continue;
|
|
8350
|
+
const j = normalizeJustification(f.label);
|
|
8351
|
+
if (j) return j;
|
|
8352
|
+
}
|
|
8353
|
+
}
|
|
8354
|
+
function firstActionRemediation(vuln, products) {
|
|
8355
|
+
const scope = new Set(products);
|
|
8356
|
+
for (const r of vuln.remediations ?? []) {
|
|
8357
|
+
const ids = r.product_ids;
|
|
8358
|
+
if (ids && ids.length > 0 && !overlap(ids, scope)) continue;
|
|
8359
|
+
if ((r.category === "vendor_fix" || r.category === "mitigation" || r.category === "workaround") && r.details) return r.details;
|
|
8360
|
+
}
|
|
8361
|
+
return "";
|
|
8362
|
+
}
|
|
8363
|
+
function buildReason$1(vuln, products) {
|
|
8364
|
+
const parts = [];
|
|
8365
|
+
const scope = new Set(products);
|
|
8366
|
+
for (const n of vuln.notes ?? []) if (n.category === "description" && n.text) parts.push(n.text);
|
|
8367
|
+
for (const t of vuln.threats ?? []) {
|
|
8368
|
+
if (!t.details) continue;
|
|
8369
|
+
const ids = t.product_ids;
|
|
8370
|
+
if (ids && ids.length > 0 && !overlap(ids, scope)) continue;
|
|
8371
|
+
parts.push(t.details);
|
|
8372
|
+
}
|
|
8373
|
+
if (parts.length === 0) return `Imported from CSAF VEX (${vuln.cve ?? "unknown CVE"})`;
|
|
8374
|
+
return parts.join("\n");
|
|
8375
|
+
}
|
|
8376
|
+
/**
|
|
8377
|
+
* Walk a CSAF product_tree and build a lookup from product_id to a
|
|
8378
|
+
* structured AffectedPackage. Prefers product_identification_helper.purl,
|
|
8379
|
+
* then .cpe, then the full_product_name's `name` (used for matching only
|
|
8380
|
+
* — name+version is hard to derive from CSAF reliably, so we drop entries
|
|
8381
|
+
* with no purl and no cpe).
|
|
8382
|
+
*/
|
|
8383
|
+
function buildProductLookup$1(tree) {
|
|
8384
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
8385
|
+
if (!tree) return lookup;
|
|
8386
|
+
if (tree.full_product_names) for (const fp of tree.full_product_names) registerProduct(fp, lookup);
|
|
8387
|
+
if (tree.branches) walkBranches(tree.branches, lookup);
|
|
8388
|
+
return lookup;
|
|
8389
|
+
}
|
|
8390
|
+
function walkBranches(branches, lookup, ctx = {}) {
|
|
8391
|
+
for (const b of branches) {
|
|
8392
|
+
const next = { ...ctx };
|
|
8393
|
+
/* c8 ignore next 2 — falsy branches require a malformed CSAF branch
|
|
8394
|
+
node (category set but name empty) which we don't fixture. */
|
|
8395
|
+
if (b.category === "product_name" && b.name) next.productName = b.name;
|
|
8396
|
+
if (b.category === "product_version" && b.name) next.version = b.name;
|
|
8397
|
+
if (b.product) registerProduct(b.product, lookup, next);
|
|
8398
|
+
if (b.branches) walkBranches(b.branches, lookup, next);
|
|
8399
|
+
}
|
|
8400
|
+
}
|
|
8401
|
+
function registerProduct(fp, lookup, ctx = {}) {
|
|
8402
|
+
/* c8 ignore next — defensive against a malformed CSAF full_product_name
|
|
8403
|
+
missing its required product_id field; not fixtured. */
|
|
8404
|
+
if (!fp.product_id) return;
|
|
8405
|
+
const helper = fp.product_identification_helper;
|
|
8406
|
+
if (helper?.purl) {
|
|
8407
|
+
const pkg = affectedPackageFromIdentifier(helper.purl);
|
|
8408
|
+
/* c8 ignore next 4 — affectedPackageFromIdentifier always returns a
|
|
8409
|
+
non-null value for an input that starts with 'pkg:' (preserves
|
|
8410
|
+
malformed input as purl-only). The null branch is unreachable from
|
|
8411
|
+
this callsite. */
|
|
8412
|
+
if (pkg) {
|
|
8413
|
+
lookup.set(fp.product_id, pkg);
|
|
8414
|
+
return;
|
|
8415
|
+
}
|
|
8416
|
+
}
|
|
8417
|
+
if (helper?.cpe) {
|
|
8418
|
+
const pkg = affectedPackageFromIdentifier(helper.cpe);
|
|
8419
|
+
/* c8 ignore next 4 — same as above; identifiers starting with
|
|
8420
|
+
'cpe:2.3:' always produce a cpe-only AffectedPackage. */
|
|
8421
|
+
if (pkg) {
|
|
8422
|
+
lookup.set(fp.product_id, pkg);
|
|
8423
|
+
return;
|
|
8424
|
+
}
|
|
8425
|
+
}
|
|
8426
|
+
if (ctx.productName && ctx.version) lookup.set(fp.product_id, {
|
|
8427
|
+
name: ctx.productName,
|
|
8428
|
+
version: ctx.version,
|
|
8429
|
+
ecosystem: Ecosystem.Generic
|
|
8430
|
+
});
|
|
8431
|
+
}
|
|
8432
|
+
function resolveAffectedPackages(productIds, lookup) {
|
|
8433
|
+
const out = [];
|
|
8434
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
8435
|
+
for (const id of productIds) {
|
|
8436
|
+
const pkg = lookup.get(id);
|
|
8437
|
+
if (!pkg) continue;
|
|
8438
|
+
const key = pkg.purl ?? pkg.cpe ?? `${pkg.name ?? ""}@${pkg.version ?? ""}`;
|
|
8439
|
+
if (seenKeys.has(key)) continue;
|
|
8440
|
+
seenKeys.add(key);
|
|
8441
|
+
out.push(pkg);
|
|
8442
|
+
}
|
|
8443
|
+
return out;
|
|
8444
|
+
}
|
|
8445
|
+
function identityFor(p) {
|
|
8446
|
+
if (!p.name) return {
|
|
8447
|
+
type: IdentityType.System,
|
|
8448
|
+
identifier: "csaf-vex-import"
|
|
8449
|
+
};
|
|
8450
|
+
const id = {
|
|
8451
|
+
type: IdentityType.Simple,
|
|
8452
|
+
identifier: p.name
|
|
8453
|
+
};
|
|
8454
|
+
if (p.category) id.description = p.category;
|
|
8455
|
+
return id;
|
|
8456
|
+
}
|
|
8457
|
+
function advisoryURI(publisher, tracking) {
|
|
8458
|
+
/* c8 ignore next */
|
|
8459
|
+
const id = tracking.id ?? "";
|
|
8460
|
+
const ns = publisher.namespace;
|
|
8461
|
+
if (ns && id) return `${ns.replace(/\/+$/, "")}/${id}`;
|
|
8462
|
+
return id;
|
|
8463
|
+
}
|
|
8464
|
+
function overlap(ids, scope) {
|
|
8465
|
+
return ids.some((id) => scope.has(id));
|
|
8466
|
+
}
|
|
8467
|
+
//#endregion
|
|
8468
|
+
//#region converters/cyclonedx-vex-to-hdf/typescript/converter.ts
|
|
8469
|
+
/**
|
|
8470
|
+
* CycloneDX VEX to HDF Amendments converter.
|
|
8471
|
+
*
|
|
8472
|
+
* CycloneDX VEX is not a separate format — it's a CycloneDX BOM whose
|
|
8473
|
+
* vulnerabilities[] carry an `analysis` object. CycloneDX-specific
|
|
8474
|
+
* justifications without an HDF equivalent (requires_configuration,
|
|
8475
|
+
* protected_by_compiler, etc.) are preserved verbatim in the reason
|
|
8476
|
+
* field via the shared helper's unknown-value passthrough.
|
|
8477
|
+
*
|
|
8478
|
+
* Spec: https://cyclonedx.org/use-cases/#vulnerability-exploitability
|
|
8479
|
+
*/
|
|
8480
|
+
const DEFAULT_EXPIRY_HORIZON_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
8481
|
+
async function convertCyclonedxVexToHdf(input, converterVersion) {
|
|
8482
|
+
validateInputSize(input, "cyclonedx-vex-to-hdf");
|
|
8483
|
+
const bom = parseJSON(input);
|
|
8484
|
+
if (bom.bomFormat !== "CycloneDX") throw new Error(`cyclonedx-vex-to-hdf: bomFormat is ${JSON.stringify(bom.bomFormat)}; only 'CycloneDX' is supported`);
|
|
8485
|
+
const docTime = (bom.metadata?.timestamp ? parseTimestamp(bom.metadata.timestamp) : null) ?? /* @__PURE__ */ new Date();
|
|
8486
|
+
const productLookup = buildProductLookup(bom);
|
|
8487
|
+
const publisher = publisherIdentityOrDefault(bom);
|
|
8488
|
+
const overrides = [];
|
|
8489
|
+
for (const v of bom.vulnerabilities ?? []) {
|
|
8490
|
+
const o = vulnerabilityToOverride(v, productLookup, docTime, publisher);
|
|
8491
|
+
if (o) overrides.push(o);
|
|
8492
|
+
}
|
|
8493
|
+
if (overrides.length === 0) throw new Error("cyclonedx-vex-to-hdf: BOM contains no actionable VEX statements (only exploitable/in_triage or no analysis); no amendment to write");
|
|
8494
|
+
const publisherName = publisher.identifier;
|
|
8495
|
+
return {
|
|
8496
|
+
name: publisherName && publisherName !== "cyclonedx-vex-import" ? `CycloneDX VEX statements from ${publisherName}` : "CycloneDX VEX statements",
|
|
8497
|
+
description: bom.serialNumber ? `Imported CycloneDX VEX ${bom.serialNumber}` : "Imported CycloneDX VEX",
|
|
8498
|
+
overrides,
|
|
8499
|
+
appliedBy: publisher,
|
|
8500
|
+
generator: {
|
|
8501
|
+
name: "cyclonedx-vex-to-hdf",
|
|
8502
|
+
version: converterVersion
|
|
8503
|
+
},
|
|
8504
|
+
integrity: await inputIntegrity(input)
|
|
8505
|
+
};
|
|
8506
|
+
}
|
|
8507
|
+
function vulnerabilityToOverride(v, productLookup, docTime, publisher) {
|
|
8508
|
+
if (!v.id || !v.analysis?.state) return void 0;
|
|
8509
|
+
const canonical = normalizeStatus(v.analysis.state);
|
|
8510
|
+
if (!canonical) return void 0;
|
|
8511
|
+
const target = importTargetFor(canonical);
|
|
8512
|
+
if (!target) return void 0;
|
|
8513
|
+
const affectedPackages = affectedPackagesForVuln(v, productLookup);
|
|
8514
|
+
const expiresAt = new Date(docTime.getTime() + DEFAULT_EXPIRY_HORIZON_MS);
|
|
8515
|
+
const override = {
|
|
8516
|
+
type: target.overrideType,
|
|
8517
|
+
requirementId: v.id,
|
|
8518
|
+
appliedAt: docTime,
|
|
8519
|
+
expiresAt,
|
|
8520
|
+
appliedBy: publisher,
|
|
8521
|
+
reason: buildReason(v)
|
|
8522
|
+
};
|
|
8523
|
+
if (affectedPackages.length > 0) override.affectedPackages = affectedPackages;
|
|
8524
|
+
if (target.status !== void 0) override.status = target.status;
|
|
8525
|
+
if (target.setJustification && v.analysis.justification) {
|
|
8526
|
+
const j = normalizeJustification(v.analysis.justification);
|
|
8527
|
+
if (j) override.justification = j;
|
|
8528
|
+
}
|
|
8529
|
+
const evidence = [];
|
|
8530
|
+
if (v.source?.url) {
|
|
8531
|
+
const ev = supplierEvidence(v.source.url, v.source.name ?? "");
|
|
8532
|
+
if (ev) evidence.push(ev);
|
|
8533
|
+
}
|
|
8534
|
+
for (const r of v.references ?? []) {
|
|
8535
|
+
if (!r.source?.url) continue;
|
|
8536
|
+
const ev = supplierEvidence(r.source.url, r.source.name ?? "");
|
|
8537
|
+
if (ev) evidence.push(ev);
|
|
8538
|
+
}
|
|
8539
|
+
if (evidence.length > 0) override.evidence = evidence;
|
|
8540
|
+
if (target.overrideType === OverrideType.Poam) override.milestones = [{
|
|
8541
|
+
description: firstActionFromResponse(v.analysis.response ?? []) || target.poamActionTemplate,
|
|
8542
|
+
status: MilestoneStatus.Pending,
|
|
8543
|
+
estimatedCompletion: expiresAt
|
|
8544
|
+
}];
|
|
8545
|
+
return override;
|
|
8546
|
+
}
|
|
8547
|
+
function buildReason(v) {
|
|
8548
|
+
const parts = [];
|
|
8549
|
+
if (v.description) parts.push(v.description);
|
|
8550
|
+
if (v.analysis?.detail) parts.push(v.analysis.detail);
|
|
8551
|
+
return parts.join("\n");
|
|
8552
|
+
}
|
|
8553
|
+
/**
|
|
8554
|
+
* Resolve CycloneDX affects[].ref entries to structured AffectedPackage
|
|
8555
|
+
* entries. Looks up the bom-ref in the component table to recover purl,
|
|
8556
|
+
* name/version. Opaque bom-refs with no component-table match are dropped
|
|
8557
|
+
* (the schema forbids fabricating name+version, and bom-refs aren't
|
|
8558
|
+
* portable identifiers outside their source BOM).
|
|
8559
|
+
*/
|
|
8560
|
+
function affectedPackagesForVuln(v, lookup) {
|
|
8561
|
+
const out = [];
|
|
8562
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
8563
|
+
for (const a of v.affects ?? []) {
|
|
8564
|
+
if (!a.ref) continue;
|
|
8565
|
+
const comp = lookup.get(a.ref);
|
|
8566
|
+
const pkg = comp ? affectedPackageFromComponent(comp) : affectedPackageFromIdentifier(a.ref);
|
|
8567
|
+
if (!pkg) continue;
|
|
8568
|
+
const key = pkg.purl ?? pkg.cpe ?? `${pkg.name ?? ""}@${pkg.version ?? ""}`;
|
|
8569
|
+
if (seenKeys.has(key)) continue;
|
|
8570
|
+
seenKeys.add(key);
|
|
8571
|
+
out.push(pkg);
|
|
8572
|
+
}
|
|
8573
|
+
return out;
|
|
8574
|
+
}
|
|
8575
|
+
/**
|
|
8576
|
+
* Build an AffectedPackage from a CycloneDX Component. Prefers structured
|
|
8577
|
+
* purl decomposition via the shared helper (so name/version/ecosystem are
|
|
8578
|
+
* also populated when the purl is parseable); falls back to the component's
|
|
8579
|
+
* name+version when no purl is set. Returns undefined when the component
|
|
8580
|
+
* carries neither a purl nor a name+version pair (schema requires at least
|
|
8581
|
+
* name+version+ecosystem, purl, or cpe).
|
|
8582
|
+
*/
|
|
8583
|
+
function affectedPackageFromComponent(c) {
|
|
8584
|
+
if (c.purl) return affectedPackageFromIdentifier(c.purl);
|
|
8585
|
+
if (c.name && c.version) return {
|
|
8586
|
+
name: c.name,
|
|
8587
|
+
version: c.version,
|
|
8588
|
+
ecosystem: Ecosystem.Generic
|
|
8589
|
+
};
|
|
8590
|
+
}
|
|
8591
|
+
function buildProductLookup(bom) {
|
|
8592
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
8593
|
+
const root = bom.metadata?.component;
|
|
8594
|
+
if (root?.["bom-ref"]) lookup.set(root["bom-ref"], root);
|
|
8595
|
+
for (const c of bom.components ?? []) if (c["bom-ref"]) lookup.set(c["bom-ref"], c);
|
|
8596
|
+
return lookup;
|
|
8597
|
+
}
|
|
8598
|
+
function firstActionFromResponse(resp) {
|
|
8599
|
+
for (const r of resp) switch (r.trim().toLowerCase()) {
|
|
8600
|
+
case "update": return "Apply vendor update and re-scan to verify.";
|
|
8601
|
+
case "rollback": return "Roll back to the unaffected version and re-scan to verify.";
|
|
8602
|
+
case "workaround_available": return "Apply the documented workaround.";
|
|
8603
|
+
}
|
|
8604
|
+
return "";
|
|
8605
|
+
}
|
|
8606
|
+
function publisherIdentityOrDefault(bom) {
|
|
8607
|
+
for (const a of bom.metadata?.authors ?? []) {
|
|
8608
|
+
if (a.email) return {
|
|
8609
|
+
type: IdentityType.Email,
|
|
8610
|
+
identifier: a.email
|
|
8611
|
+
};
|
|
8612
|
+
if (a.name) return {
|
|
8613
|
+
type: IdentityType.Simple,
|
|
8614
|
+
identifier: a.name
|
|
8615
|
+
};
|
|
8616
|
+
}
|
|
8617
|
+
for (const t of bom.metadata?.tools ?? []) {
|
|
8618
|
+
const ident = [t.vendor, t.name].filter(Boolean).join(" ").trim();
|
|
8619
|
+
if (ident) return {
|
|
8620
|
+
type: IdentityType.System,
|
|
8621
|
+
identifier: ident
|
|
8622
|
+
};
|
|
8623
|
+
}
|
|
8624
|
+
return {
|
|
8625
|
+
type: IdentityType.System,
|
|
8626
|
+
identifier: "cyclonedx-vex-import"
|
|
8627
|
+
};
|
|
8628
|
+
}
|
|
8629
|
+
//#endregion
|
|
8630
|
+
//#region converters/hdf-to-csaf-vex/typescript/converter.ts
|
|
8631
|
+
/**
|
|
8632
|
+
* HDF Amendments to CSAF VEX (csaf_vex profile) converter.
|
|
8633
|
+
*
|
|
8634
|
+
* Reverse direction of csaf-vex-to-hdf. Intentionally partial-fidelity:
|
|
8635
|
+
* fields the shared VEX mapping considers consumer-action-bearing survive
|
|
8636
|
+
* round-trip; the rest collapse into the available CSAF fields.
|
|
8637
|
+
*/
|
|
8638
|
+
const CVE_ID_PATTERN$2 = /^CVE-\d{4}-\d{4,}$/;
|
|
8639
|
+
const PRODUCTS_LINE$2 = /^Products:\s*(.+)$/m;
|
|
8640
|
+
const DEFAULT_PRODUCT_ID$2 = "HDFPID-0001";
|
|
8641
|
+
function convertHdfToCsafVex(input, converterVersion) {
|
|
8642
|
+
validateInputSize(input, "hdf-to-csaf-vex");
|
|
8643
|
+
const amendments = parseJSON(input);
|
|
8644
|
+
const groups = groupByCVE(amendments.overrides ?? []);
|
|
8645
|
+
const productSet = /* @__PURE__ */ new Map();
|
|
8646
|
+
const vulnerabilities = [];
|
|
8647
|
+
for (const group of groups) {
|
|
8648
|
+
const v = buildVulnerability(group);
|
|
8649
|
+
if (!v) continue;
|
|
8650
|
+
vulnerabilities.push(v);
|
|
8651
|
+
for (const p of productIDsForGroup(group)) productSet.set(p, true);
|
|
8652
|
+
}
|
|
8653
|
+
if (vulnerabilities.length === 0) throw new Error("hdf-to-csaf-vex: no overrides with CVE-shaped requirementIds; nothing to emit");
|
|
8654
|
+
const products = [...productSet.keys()].sort();
|
|
8655
|
+
const doc = buildDocument(amendments, converterVersion);
|
|
8656
|
+
doc.vulnerabilities = vulnerabilities;
|
|
8657
|
+
doc.product_tree.full_product_names = products.length > 0 ? products.map((p) => ({
|
|
8658
|
+
name: p,
|
|
8659
|
+
product_id: p
|
|
8660
|
+
})) : [{
|
|
8661
|
+
name: DEFAULT_PRODUCT_ID$2,
|
|
8662
|
+
product_id: DEFAULT_PRODUCT_ID$2
|
|
8663
|
+
}];
|
|
8664
|
+
return JSON.stringify(doc, null, 2);
|
|
8665
|
+
}
|
|
8666
|
+
function groupByCVE(overrides) {
|
|
8667
|
+
const groups = /* @__PURE__ */ new Map();
|
|
8668
|
+
for (const o of overrides) {
|
|
8669
|
+
if (!CVE_ID_PATTERN$2.test(o.requirementId)) continue;
|
|
8670
|
+
let g = groups.get(o.requirementId);
|
|
8671
|
+
if (!g) {
|
|
8672
|
+
g = {
|
|
8673
|
+
cve: o.requirementId,
|
|
8674
|
+
overrides: []
|
|
8675
|
+
};
|
|
8676
|
+
groups.set(o.requirementId, g);
|
|
8677
|
+
}
|
|
8678
|
+
g.overrides.push(o);
|
|
8679
|
+
}
|
|
8680
|
+
return [...groups.values()].sort((a, b) => a.cve.localeCompare(b.cve));
|
|
8681
|
+
}
|
|
8682
|
+
function productIDsForGroup(group) {
|
|
8683
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8684
|
+
for (const o of group.overrides) for (const p of productIDsFor$1(o)) seen.add(p);
|
|
8685
|
+
return [...seen].sort();
|
|
8686
|
+
}
|
|
8687
|
+
function productIDsFor$1(o) {
|
|
8688
|
+
if (o.affectedPackages && o.affectedPackages.length > 0) {
|
|
8689
|
+
const ids = o.affectedPackages.map((p) => affectedPackageToIdentifier(p)).filter((id) => Boolean(id));
|
|
8690
|
+
if (ids.length > 0) return ids;
|
|
8691
|
+
}
|
|
8692
|
+
if (o.componentRef) return [o.componentRef];
|
|
8693
|
+
const m = PRODUCTS_LINE$2.exec(o.reason ?? "");
|
|
8694
|
+
if (m && m[1]) {
|
|
8695
|
+
const parts = m[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
8696
|
+
if (parts.length > 0) return parts;
|
|
8697
|
+
}
|
|
8698
|
+
return [DEFAULT_PRODUCT_ID$2];
|
|
8699
|
+
}
|
|
8700
|
+
function stripProductsLine$1(reason) {
|
|
8701
|
+
return reason.replace(PRODUCTS_LINE$2, "").replace(/\n+$/, "");
|
|
8702
|
+
}
|
|
8703
|
+
function allMilestonesCompleted$2(o) {
|
|
8704
|
+
if (!o.milestones || o.milestones.length === 0) return false;
|
|
8705
|
+
return o.milestones.every((m) => m.status === MilestoneStatus.Completed);
|
|
8706
|
+
}
|
|
8707
|
+
function buildVulnerability(group) {
|
|
8708
|
+
const v = { cve: group.cve };
|
|
8709
|
+
const status = {};
|
|
8710
|
+
let emitted = false;
|
|
8711
|
+
for (const o of group.overrides) {
|
|
8712
|
+
const pids = productIDsFor$1(o);
|
|
8713
|
+
let canonical = exportStatusFor(o, allMilestonesCompleted$2(o), false);
|
|
8714
|
+
if (!canonical) continue;
|
|
8715
|
+
if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted$2(o)) canonical = VexStatus.Fixed;
|
|
8716
|
+
if (canonical === VexStatus.NotAffected) {
|
|
8717
|
+
status.known_not_affected = (status.known_not_affected ?? []).concat(pids);
|
|
8718
|
+
if (o.justification) {
|
|
8719
|
+
v.flags = v.flags ?? [];
|
|
8720
|
+
v.flags.push({
|
|
8721
|
+
label: String(o.justification),
|
|
8722
|
+
product_ids: pids,
|
|
8723
|
+
date: formatTimestampSeconds(new Date(o.appliedAt))
|
|
8724
|
+
});
|
|
8725
|
+
}
|
|
8726
|
+
emitted = true;
|
|
8727
|
+
} else if (canonical === VexStatus.Fixed) {
|
|
8728
|
+
status.fixed = (status.fixed ?? []).concat(pids);
|
|
8729
|
+
for (const m of o.milestones ?? []) {
|
|
8730
|
+
if (!m.description) continue;
|
|
8731
|
+
v.remediations = v.remediations ?? [];
|
|
8732
|
+
v.remediations.push({
|
|
8733
|
+
category: "vendor_fix",
|
|
8734
|
+
details: m.description,
|
|
8735
|
+
product_ids: pids
|
|
8736
|
+
});
|
|
8737
|
+
}
|
|
8738
|
+
emitted = true;
|
|
8739
|
+
} else if (canonical === VexStatus.Affected) {
|
|
8740
|
+
status.known_affected = (status.known_affected ?? []).concat(pids);
|
|
8741
|
+
if (o.reason) {
|
|
8742
|
+
v.threats = v.threats ?? [];
|
|
8743
|
+
v.threats.push({
|
|
8744
|
+
category: "impact",
|
|
8745
|
+
details: stripProductsLine$1(o.reason),
|
|
8746
|
+
product_ids: pids
|
|
8747
|
+
});
|
|
8748
|
+
}
|
|
8749
|
+
if (o.type === OverrideType.Poam) for (const m of o.milestones ?? []) {
|
|
8750
|
+
if (!m.description) continue;
|
|
8751
|
+
v.remediations = v.remediations ?? [];
|
|
8752
|
+
v.remediations.push({
|
|
8753
|
+
category: "workaround",
|
|
8754
|
+
details: m.description,
|
|
8755
|
+
product_ids: pids
|
|
8756
|
+
});
|
|
8757
|
+
}
|
|
8758
|
+
emitted = true;
|
|
8759
|
+
}
|
|
8760
|
+
for (const e of o.evidence ?? []) {
|
|
8761
|
+
if (e.type !== "url" || !e.data) continue;
|
|
8762
|
+
v.references = v.references ?? [];
|
|
8763
|
+
v.references.push({
|
|
8764
|
+
category: "external",
|
|
8765
|
+
summary: e.description ?? "",
|
|
8766
|
+
url: e.data
|
|
8767
|
+
});
|
|
8768
|
+
}
|
|
8769
|
+
}
|
|
8770
|
+
/* c8 ignore next */
|
|
8771
|
+
if (!emitted) return void 0;
|
|
8772
|
+
if (status.fixed) status.fixed = [...new Set(status.fixed)];
|
|
8773
|
+
if (status.known_affected) status.known_affected = [...new Set(status.known_affected)];
|
|
8774
|
+
if (status.known_not_affected) status.known_not_affected = [...new Set(status.known_not_affected)];
|
|
8775
|
+
v.product_status = status;
|
|
8776
|
+
if (v.references) {
|
|
8777
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8778
|
+
v.references = v.references.filter((r) => {
|
|
8779
|
+
if (seen.has(r.url)) return false;
|
|
8780
|
+
seen.add(r.url);
|
|
8781
|
+
return true;
|
|
8782
|
+
});
|
|
8783
|
+
}
|
|
8784
|
+
return v;
|
|
8785
|
+
}
|
|
8786
|
+
function buildDocument(amendments, converterVersion) {
|
|
8787
|
+
const now = formatTimestampSeconds(/* @__PURE__ */ new Date());
|
|
8788
|
+
const publisherName = amendments.appliedBy?.identifier || "HDF Amendments Export";
|
|
8789
|
+
const trackingId = amendments.amendmentId || "HDF-VEX-EXPORT";
|
|
8790
|
+
const docVersion = amendments.version || "1";
|
|
8791
|
+
const title = amendments.name || "HDF Amendments exported as CSAF VEX";
|
|
8792
|
+
const notes = [];
|
|
8793
|
+
if (amendments.description) notes.push({
|
|
8794
|
+
category: "summary",
|
|
8795
|
+
text: amendments.description,
|
|
8796
|
+
title: "Description"
|
|
8797
|
+
});
|
|
8798
|
+
return {
|
|
8799
|
+
document: {
|
|
8800
|
+
category: "csaf_vex",
|
|
8801
|
+
csaf_version: "2.0",
|
|
8802
|
+
title,
|
|
8803
|
+
notes,
|
|
8804
|
+
publisher: {
|
|
8805
|
+
category: "other",
|
|
8806
|
+
name: publisherName
|
|
8807
|
+
},
|
|
8808
|
+
tracking: {
|
|
8809
|
+
id: trackingId,
|
|
8810
|
+
status: "final",
|
|
8811
|
+
version: docVersion,
|
|
8812
|
+
current_release_date: now,
|
|
8813
|
+
initial_release_date: now,
|
|
8814
|
+
revision_history: [{
|
|
8815
|
+
date: now,
|
|
8816
|
+
number: docVersion,
|
|
8817
|
+
summary: "Generated by hdf-to-csaf-vex from HDF Amendments."
|
|
8818
|
+
}],
|
|
8819
|
+
generator: {
|
|
8820
|
+
engine: {
|
|
8821
|
+
name: "hdf-to-csaf-vex",
|
|
8822
|
+
version: converterVersion
|
|
8823
|
+
},
|
|
8824
|
+
date: now
|
|
8825
|
+
}
|
|
8826
|
+
}
|
|
8827
|
+
},
|
|
8828
|
+
product_tree: { full_product_names: [] },
|
|
8829
|
+
vulnerabilities: []
|
|
8830
|
+
};
|
|
8831
|
+
}
|
|
8832
|
+
//#endregion
|
|
8833
|
+
//#region converters/hdf-to-openvex/typescript/converter.ts
|
|
8834
|
+
/**
|
|
8835
|
+
* HDF Amendments to OpenVEX converter.
|
|
8836
|
+
*
|
|
8837
|
+
* Reverse direction of openvex-to-hdf. Step 4f amendment-output pattern,
|
|
8838
|
+
* partial-fidelity by design — consumer-action-bearing fields (CVE id,
|
|
8839
|
+
* status, justification) survive round-trip; the rest collapse.
|
|
8840
|
+
*/
|
|
8841
|
+
const CVE_ID_PATTERN$1 = /^CVE-\d{4}-\d{4,}$/;
|
|
8842
|
+
const PRODUCTS_LINE$1 = /^Products:\s*(.+)$/m;
|
|
8843
|
+
const OPENVEX_CONTEXT = "https://openvex.dev/ns/v0.2.0";
|
|
8844
|
+
const OPENVEX_NAMESPACE = "https://openvex.dev/docs/public/";
|
|
8845
|
+
const DEFAULT_PRODUCT_ID$1 = "HDFPID-0001";
|
|
8846
|
+
async function convertHdfToOpenVex(input, converterVersion) {
|
|
8847
|
+
validateInputSize(input, "hdf-to-openvex");
|
|
8848
|
+
const amendments = parseJSON(input);
|
|
8849
|
+
const statements = [];
|
|
8850
|
+
let earliest;
|
|
8851
|
+
for (const o of amendments.overrides ?? []) {
|
|
8852
|
+
const s = overrideToStatement(o);
|
|
8853
|
+
if (!s) continue;
|
|
8854
|
+
statements.push(s);
|
|
8855
|
+
const t = new Date(o.appliedAt);
|
|
8856
|
+
if (!earliest || t < earliest) earliest = t;
|
|
8857
|
+
}
|
|
8858
|
+
if (statements.length === 0) throw new Error("hdf-to-openvex: no overrides with CVE-shaped requirementIds; nothing to emit");
|
|
8859
|
+
statements.sort((a, b) => a.vulnerability.name.localeCompare(b.vulnerability.name));
|
|
8860
|
+
const author = amendments.appliedBy?.identifier || "HDF Amendments Export";
|
|
8861
|
+
const role = amendments.appliedBy?.description;
|
|
8862
|
+
const doc = {
|
|
8863
|
+
"@context": OPENVEX_CONTEXT,
|
|
8864
|
+
"@id": await buildDocumentID(input, amendments),
|
|
8865
|
+
author,
|
|
8866
|
+
role,
|
|
8867
|
+
timestamp: formatTimestampSeconds(earliest ?? /* @__PURE__ */ new Date()),
|
|
8868
|
+
version: 1,
|
|
8869
|
+
statements
|
|
8870
|
+
};
|
|
8871
|
+
return JSON.stringify(doc, null, 2);
|
|
8872
|
+
}
|
|
8873
|
+
function overrideToStatement(o) {
|
|
8874
|
+
if (!CVE_ID_PATTERN$1.test(o.requirementId)) return void 0;
|
|
8875
|
+
let canonical = exportStatusFor(o, allMilestonesCompleted$1(o), false);
|
|
8876
|
+
if (!canonical) return void 0;
|
|
8877
|
+
if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted$1(o)) canonical = VexStatus.Fixed;
|
|
8878
|
+
const stmt = {
|
|
8879
|
+
vulnerability: {
|
|
8880
|
+
name: o.requirementId,
|
|
8881
|
+
"@id": `https://nvd.nist.gov/vuln/detail/${o.requirementId}`
|
|
8882
|
+
},
|
|
8883
|
+
status: String(canonical),
|
|
8884
|
+
timestamp: formatTimestampSeconds(new Date(o.appliedAt)),
|
|
8885
|
+
products: productsFor(o)
|
|
8886
|
+
};
|
|
8887
|
+
if (canonical === VexStatus.NotAffected) {
|
|
8888
|
+
if (o.justification) stmt.justification = String(o.justification);
|
|
8889
|
+
const impact = stripProductsLine(o.reason ?? "");
|
|
8890
|
+
if (impact) stmt.impact_statement = impact;
|
|
8891
|
+
} else if (canonical === VexStatus.Fixed) stmt.action_statement = firstMilestoneAction(o) || "Fix applied; consumer re-scan confirmed clean.";
|
|
8892
|
+
else if (canonical === VexStatus.Affected) stmt.action_statement = firstMilestoneAction(o) || stripProductsLine(o.reason ?? "");
|
|
8893
|
+
return stmt;
|
|
8894
|
+
}
|
|
8895
|
+
function productsFor(o) {
|
|
8896
|
+
if (o.affectedPackages && o.affectedPackages.length > 0) {
|
|
8897
|
+
const ids = o.affectedPackages.map((p) => affectedPackageToIdentifier(p)).filter((id) => Boolean(id));
|
|
8898
|
+
if (ids.length > 0) return ids.map((id) => ({ "@id": id }));
|
|
8899
|
+
}
|
|
8900
|
+
let ids = [];
|
|
8901
|
+
if (o.componentRef) ids = [o.componentRef];
|
|
8902
|
+
else {
|
|
8903
|
+
const m = PRODUCTS_LINE$1.exec(o.reason ?? "");
|
|
8904
|
+
if (m && m[1]) ids = m[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
8905
|
+
}
|
|
8906
|
+
if (ids.length === 0) ids = [DEFAULT_PRODUCT_ID$1];
|
|
8907
|
+
return ids.map((id) => ({ "@id": id }));
|
|
8908
|
+
}
|
|
8909
|
+
function firstMilestoneAction(o) {
|
|
8910
|
+
for (const m of o.milestones ?? []) if (m.description) return m.description;
|
|
8911
|
+
return "";
|
|
8912
|
+
}
|
|
8913
|
+
function allMilestonesCompleted$1(o) {
|
|
8914
|
+
if (!o.milestones || o.milestones.length === 0) return false;
|
|
8915
|
+
return o.milestones.every((m) => m.status === MilestoneStatus.Completed);
|
|
8916
|
+
}
|
|
8917
|
+
function stripProductsLine(reason) {
|
|
8918
|
+
return reason.replace(PRODUCTS_LINE$1, "").replace(/\n+$/, "");
|
|
8919
|
+
}
|
|
8920
|
+
async function buildDocumentID(input, a) {
|
|
8921
|
+
if (a.amendmentId) return `${OPENVEX_NAMESPACE}vex-${a.amendmentId}`;
|
|
8922
|
+
return `${OPENVEX_NAMESPACE}vex-${await sha256(input)}`;
|
|
8923
|
+
}
|
|
8924
|
+
//#endregion
|
|
8925
|
+
//#region converters/hdf-to-cyclonedx-vex/typescript/converter.ts
|
|
8926
|
+
/**
|
|
8927
|
+
* HDF Amendments to CycloneDX VEX converter (export side).
|
|
8928
|
+
*
|
|
8929
|
+
* Reverse direction of cyclonedx-vex-to-hdf. Step 4f amendment-output
|
|
8930
|
+
* pattern, partial-fidelity by design — consumer-action-bearing fields
|
|
8931
|
+
* (CVE id, status, justification) survive round-trip; the rest collapse
|
|
8932
|
+
* into the available CycloneDX VEX fields.
|
|
8933
|
+
*/
|
|
8934
|
+
const CVE_ID_PATTERN = /^CVE-\d{4}-\d{4,}$/;
|
|
8935
|
+
const PRODUCTS_LINE = /^Products:\s*(.+)$/m;
|
|
8936
|
+
const RAW_JUST_LINE = /^VEX justification:\s*(.+)$/m;
|
|
8937
|
+
const RESPONSE_LINE = /^Response:.*$/gm;
|
|
8938
|
+
const DEFAULT_PRODUCT_ID = "HDFPID-0001";
|
|
8939
|
+
function convertHdfToCyclonedxVex(input, converterVersion) {
|
|
8940
|
+
validateInputSize(input, "hdf-to-cyclonedx-vex");
|
|
8941
|
+
const amendments = parseJSON(input);
|
|
8942
|
+
const componentRegistry = /* @__PURE__ */ new Map();
|
|
8943
|
+
const vulnerabilities = [];
|
|
8944
|
+
let earliest;
|
|
8945
|
+
for (const o of amendments.overrides ?? []) {
|
|
8946
|
+
if (!CVE_ID_PATTERN.test(o.requirementId)) continue;
|
|
8947
|
+
const v = overrideToVulnerability(o, componentRegistry);
|
|
8948
|
+
if (!v) continue;
|
|
8949
|
+
vulnerabilities.push(v);
|
|
8950
|
+
const t = new Date(o.appliedAt);
|
|
8951
|
+
if (!earliest || t < earliest) earliest = t;
|
|
8952
|
+
}
|
|
8953
|
+
if (vulnerabilities.length === 0) throw new Error("hdf-to-cyclonedx-vex: no overrides with CVE-shaped requirementIds; nothing to emit");
|
|
8954
|
+
vulnerabilities.sort((a, b) => a.id.localeCompare(b.id));
|
|
8955
|
+
const components = [...componentRegistry.values()].sort((a, b) => a["bom-ref"].localeCompare(b["bom-ref"]));
|
|
8956
|
+
const bom = {
|
|
8957
|
+
bomFormat: "CycloneDX",
|
|
8958
|
+
specVersion: "1.4",
|
|
8959
|
+
serialNumber: buildSerialNumberSync(input, amendments),
|
|
8960
|
+
version: 1,
|
|
8961
|
+
metadata: buildMetadata(amendments, earliest ?? /* @__PURE__ */ new Date(), converterVersion),
|
|
8962
|
+
components,
|
|
8963
|
+
vulnerabilities
|
|
8964
|
+
};
|
|
8965
|
+
return JSON.stringify(bom, null, 2);
|
|
8966
|
+
}
|
|
8967
|
+
function overrideToVulnerability(o, componentRegistry) {
|
|
8968
|
+
let canonical = exportStatusFor(o, allMilestonesCompleted(o), false);
|
|
8969
|
+
if (!canonical) return void 0;
|
|
8970
|
+
if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted(o)) canonical = VexStatus.Fixed;
|
|
8971
|
+
const pids = productIDsFor(o);
|
|
8972
|
+
const pkgById = /* @__PURE__ */ new Map();
|
|
8973
|
+
for (const p of o.affectedPackages ?? []) {
|
|
8974
|
+
const id = affectedPackageToIdentifier(p);
|
|
8975
|
+
if (id) pkgById.set(id, p);
|
|
8976
|
+
}
|
|
8977
|
+
for (const pid of pids) componentRegistry.set(pid, componentFor(pid, pkgById.get(pid)));
|
|
8978
|
+
const analysis = { state: canonicalToCycloneDXState(canonical) };
|
|
8979
|
+
if (o.justification) {
|
|
8980
|
+
const cdxJust = justificationForCycloneDX(o.justification);
|
|
8981
|
+
if (cdxJust) analysis.justification = cdxJust;
|
|
8982
|
+
}
|
|
8983
|
+
const detail = stripReasonAnnotations(o.reason ?? "");
|
|
8984
|
+
if (detail) analysis.detail = detail;
|
|
8985
|
+
if (canonical === VexStatus.Fixed) analysis.response = ["update"];
|
|
8986
|
+
else if (canonical === VexStatus.Affected && o.type === OverrideType.Poam) analysis.response = ["workaround_available"];
|
|
8987
|
+
const v = {
|
|
8988
|
+
id: o.requirementId,
|
|
8989
|
+
source: {
|
|
8990
|
+
name: "NVD",
|
|
8991
|
+
url: `https://nvd.nist.gov/vuln/detail/${o.requirementId}`
|
|
8992
|
+
},
|
|
8993
|
+
analysis,
|
|
8994
|
+
affects: pids.map((p) => ({ ref: p }))
|
|
8995
|
+
};
|
|
8996
|
+
for (const e of o.evidence ?? []) {
|
|
8997
|
+
if (e.type !== "url" || !e.data) continue;
|
|
8998
|
+
v.references = v.references ?? [];
|
|
8999
|
+
v.references.push({
|
|
9000
|
+
id: o.requirementId,
|
|
9001
|
+
source: {
|
|
9002
|
+
name: e.description ?? "",
|
|
9003
|
+
url: e.data
|
|
9004
|
+
}
|
|
9005
|
+
});
|
|
9006
|
+
}
|
|
9007
|
+
return v;
|
|
9008
|
+
}
|
|
9009
|
+
function canonicalToCycloneDXState(canonical) {
|
|
9010
|
+
switch (canonical) {
|
|
9011
|
+
case VexStatus.NotAffected: return "not_affected";
|
|
9012
|
+
case VexStatus.Fixed: return "resolved";
|
|
9013
|
+
case VexStatus.Affected: return "exploitable";
|
|
9014
|
+
/* c8 ignore next 2 — defensive: every VexStatus has a case above */
|
|
9015
|
+
default: return canonical;
|
|
9016
|
+
}
|
|
9017
|
+
}
|
|
9018
|
+
function componentFor(pid, pkg) {
|
|
9019
|
+
const c = {
|
|
9020
|
+
type: "application",
|
|
9021
|
+
name: pkg?.name ?? pid,
|
|
9022
|
+
"bom-ref": pid
|
|
9023
|
+
};
|
|
9024
|
+
if (pkg?.version) c.version = pkg.version;
|
|
9025
|
+
if (pkg?.purl ?? (pid.startsWith("pkg:") ? pid : void 0)) c.purl = pkg?.purl ?? pid;
|
|
9026
|
+
if (pkg?.cpe ?? (pid.startsWith("cpe:2.3:") ? pid : void 0)) c.cpe = pkg?.cpe ?? pid;
|
|
9027
|
+
return c;
|
|
9028
|
+
}
|
|
9029
|
+
function productIDsFor(o) {
|
|
9030
|
+
if (o.affectedPackages && o.affectedPackages.length > 0) {
|
|
9031
|
+
const ids = o.affectedPackages.map((p) => affectedPackageToIdentifier(p)).filter((id) => Boolean(id));
|
|
9032
|
+
if (ids.length > 0) return ids;
|
|
9033
|
+
}
|
|
9034
|
+
if (o.componentRef) return [o.componentRef];
|
|
9035
|
+
const m = PRODUCTS_LINE.exec(o.reason ?? "");
|
|
9036
|
+
if (m && m[1]) {
|
|
9037
|
+
const parts = m[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
9038
|
+
if (parts.length > 0) return parts;
|
|
9039
|
+
}
|
|
9040
|
+
return [DEFAULT_PRODUCT_ID];
|
|
9041
|
+
}
|
|
9042
|
+
function stripReasonAnnotations(reason) {
|
|
9043
|
+
return reason.replace(PRODUCTS_LINE, "").replace(RAW_JUST_LINE, "").replace(RESPONSE_LINE, "").trim();
|
|
9044
|
+
}
|
|
9045
|
+
function allMilestonesCompleted(o) {
|
|
9046
|
+
if (!o.milestones || o.milestones.length === 0) return false;
|
|
9047
|
+
return o.milestones.every((m) => m.status === MilestoneStatus.Completed);
|
|
9048
|
+
}
|
|
9049
|
+
function buildMetadata(a, docTime, converterVersion) {
|
|
9050
|
+
const metadata = {
|
|
9051
|
+
timestamp: formatTimestampSeconds(docTime),
|
|
9052
|
+
tools: [{
|
|
9053
|
+
vendor: "mitre",
|
|
9054
|
+
name: "hdf-to-cyclonedx-vex",
|
|
9055
|
+
version: converterVersion
|
|
9056
|
+
}]
|
|
9057
|
+
};
|
|
9058
|
+
if (a.appliedBy?.identifier) {
|
|
9059
|
+
const author = {};
|
|
9060
|
+
if (a.appliedBy.type === IdentityType.Email) author.email = a.appliedBy.identifier;
|
|
9061
|
+
else author.name = a.appliedBy.identifier;
|
|
9062
|
+
metadata.authors = [author];
|
|
9063
|
+
}
|
|
9064
|
+
return metadata;
|
|
9065
|
+
}
|
|
9066
|
+
function buildSerialNumberSync(input, a) {
|
|
9067
|
+
if (a.amendmentId) return `urn:uuid:${a.amendmentId}`;
|
|
9068
|
+
return `urn:uuid:${fnv1a(input)}`;
|
|
9069
|
+
}
|
|
9070
|
+
function fnv1a(s) {
|
|
9071
|
+
let h = 2166136261;
|
|
9072
|
+
for (let i = 0; i < s.length; i++) {
|
|
9073
|
+
h ^= s.charCodeAt(i);
|
|
9074
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
9075
|
+
}
|
|
9076
|
+
const d = h.toString(16).padStart(8, "0");
|
|
9077
|
+
return (d + d + d + d).slice(0, 32);
|
|
9078
|
+
}
|
|
9079
|
+
//#endregion
|
|
7082
9080
|
//#region converters/oscal-to-hdf/typescript/detect.ts
|
|
7083
9081
|
/**
|
|
7084
9082
|
* OSCAL document type detection.
|
|
@@ -7167,7 +9165,7 @@ async function catalogToBaseline(catalog, rawInput) {
|
|
|
7167
9165
|
requirements,
|
|
7168
9166
|
groups,
|
|
7169
9167
|
generator: {
|
|
7170
|
-
name: "hdf
|
|
9168
|
+
name: "oscal-catalog-to-hdf",
|
|
7171
9169
|
version: "1.0.0"
|
|
7172
9170
|
}
|
|
7173
9171
|
};
|
|
@@ -7410,7 +9408,7 @@ async function convertOscalComponentToHdf(input) {
|
|
|
7410
9408
|
integrity,
|
|
7411
9409
|
requirements,
|
|
7412
9410
|
generator: {
|
|
7413
|
-
name: "hdf
|
|
9411
|
+
name: "oscal-component-to-hdf",
|
|
7414
9412
|
version: "1.0.0"
|
|
7415
9413
|
}
|
|
7416
9414
|
};
|
|
@@ -7471,7 +9469,7 @@ async function convertOscalSspToHdf(input) {
|
|
|
7471
9469
|
integrity,
|
|
7472
9470
|
components: [],
|
|
7473
9471
|
generator: {
|
|
7474
|
-
name: "hdf
|
|
9472
|
+
name: "oscal-ssp-to-hdf",
|
|
7475
9473
|
version: "1.0.0"
|
|
7476
9474
|
}
|
|
7477
9475
|
};
|
|
@@ -7598,13 +9596,13 @@ function sspComponentToHDFComponent(sc, componentControls) {
|
|
|
7598
9596
|
function mapOSCALComponentType(oscalType) {
|
|
7599
9597
|
switch (oscalType.toLowerCase()) {
|
|
7600
9598
|
case "software":
|
|
7601
|
-
case "this-system": return
|
|
7602
|
-
case "service": return
|
|
7603
|
-
case "hardware": return
|
|
7604
|
-
case "network": return
|
|
7605
|
-
case "database": return
|
|
7606
|
-
case "storage": return
|
|
7607
|
-
default: return
|
|
9599
|
+
case "this-system": return TargetType.Application;
|
|
9600
|
+
case "service": return TargetType.Application;
|
|
9601
|
+
case "hardware": return TargetType.Host;
|
|
9602
|
+
case "network": return TargetType.Network;
|
|
9603
|
+
case "database": return TargetType.Database;
|
|
9604
|
+
case "storage": return TargetType.Artifact;
|
|
9605
|
+
default: return TargetType.Application;
|
|
7608
9606
|
}
|
|
7609
9607
|
}
|
|
7610
9608
|
//#endregion
|
|
@@ -7641,7 +9639,7 @@ async function convertOscalSapToHdf(input) {
|
|
|
7641
9639
|
type: planType,
|
|
7642
9640
|
description,
|
|
7643
9641
|
generator: {
|
|
7644
|
-
name: "hdf
|
|
9642
|
+
name: "oscal-sap-to-hdf",
|
|
7645
9643
|
version: "1.0.0"
|
|
7646
9644
|
}
|
|
7647
9645
|
};
|
|
@@ -7763,7 +9761,7 @@ async function convertOscalPoamToHdf(input) {
|
|
|
7763
9761
|
for (const item of poam["poam-items"]) overrides.push(poamItemToOverride(item, riskMap, poam));
|
|
7764
9762
|
const systemRef = poam["import-ssp"]?.href || void 0;
|
|
7765
9763
|
const appliedBy = extractAppliedBy(poam.metadata);
|
|
7766
|
-
|
|
9764
|
+
return serializeHdf({
|
|
7767
9765
|
name: toKebabCase(poam.metadata.title, "oscal-poam"),
|
|
7768
9766
|
overrides,
|
|
7769
9767
|
integrity,
|
|
@@ -7771,11 +9769,10 @@ async function convertOscalPoamToHdf(input) {
|
|
|
7771
9769
|
version: meta.version,
|
|
7772
9770
|
appliedBy,
|
|
7773
9771
|
generator: {
|
|
7774
|
-
name: "hdf
|
|
9772
|
+
name: "oscal-poam-to-hdf",
|
|
7775
9773
|
version: "1.0.0"
|
|
7776
9774
|
}
|
|
7777
|
-
};
|
|
7778
|
-
return JSON.stringify(amendments, null, 2);
|
|
9775
|
+
});
|
|
7779
9776
|
}
|
|
7780
9777
|
function buildRiskMap$1(risks) {
|
|
7781
9778
|
const m = /* @__PURE__ */ new Map();
|
|
@@ -7848,8 +9845,8 @@ function poamItemAppliedBy(poam) {
|
|
|
7848
9845
|
}
|
|
7849
9846
|
function poamItemAppliedAt(poam) {
|
|
7850
9847
|
if (poam.metadata["last-modified"]) {
|
|
7851
|
-
const t =
|
|
7852
|
-
if (
|
|
9848
|
+
const t = parseTimestamp(String(poam.metadata["last-modified"]));
|
|
9849
|
+
if (t) return t;
|
|
7853
9850
|
}
|
|
7854
9851
|
return /* @__PURE__ */ new Date();
|
|
7855
9852
|
}
|
|
@@ -7907,18 +9904,23 @@ async function convertOscalSarToHdf(input) {
|
|
|
7907
9904
|
if (!doc["assessment-results"]) throw new Error("oscal-assessment-results: input is not an assessment-results document (root key is not 'assessment-results')");
|
|
7908
9905
|
const sar = doc["assessment-results"];
|
|
7909
9906
|
const meta = extractMetadata(sar.metadata);
|
|
9907
|
+
const scanTime = /* @__PURE__ */ new Date();
|
|
7910
9908
|
const baselines = [];
|
|
7911
9909
|
for (const result of sar.results) {
|
|
7912
|
-
|
|
9910
|
+
if (!result.findings || result.findings.length === 0) {
|
|
9911
|
+
console.warn(`WARNING: Skipping assessment result "${result.title || result.uuid}": no findings (empty result set)`);
|
|
9912
|
+
continue;
|
|
9913
|
+
}
|
|
9914
|
+
const baseline = await resultToEvaluatedBaseline(result, sar, input, scanTime);
|
|
7913
9915
|
baselines.push(baseline);
|
|
7914
9916
|
}
|
|
7915
9917
|
const planRef = sar["import-ap"]?.href || void 0;
|
|
7916
9918
|
let timestamp;
|
|
7917
9919
|
if (meta.lastModified) {
|
|
7918
|
-
const t =
|
|
7919
|
-
if (
|
|
9920
|
+
const t = parseTimestamp(meta.lastModified);
|
|
9921
|
+
if (t) timestamp = t;
|
|
7920
9922
|
}
|
|
7921
|
-
return
|
|
9923
|
+
return serializeHdf({
|
|
7922
9924
|
baselines,
|
|
7923
9925
|
generator: {
|
|
7924
9926
|
name: "oscal-assessment-results-to-hdf",
|
|
@@ -7930,9 +9932,9 @@ async function convertOscalSarToHdf(input) {
|
|
|
7930
9932
|
},
|
|
7931
9933
|
timestamp: timestamp ?? /* @__PURE__ */ new Date(),
|
|
7932
9934
|
planRef
|
|
7933
|
-
}
|
|
9935
|
+
});
|
|
7934
9936
|
}
|
|
7935
|
-
async function resultToEvaluatedBaseline(result, sar, rawInput) {
|
|
9937
|
+
async function resultToEvaluatedBaseline(result, sar, rawInput, scanTime) {
|
|
7936
9938
|
const checksum = await inputChecksum(rawInput);
|
|
7937
9939
|
const obsMap = buildObservationMap(result.observations ?? []);
|
|
7938
9940
|
const riskMap = buildRiskMap(result.risks ?? []);
|
|
@@ -7949,7 +9951,7 @@ async function resultToEvaluatedBaseline(result, sar, rawInput) {
|
|
|
7949
9951
|
}
|
|
7950
9952
|
const requirements = [];
|
|
7951
9953
|
for (const controlId of controlOrder) {
|
|
7952
|
-
const req = findingsToEvaluatedRequirement(controlId, controlMap.get(controlId), obsMap, riskMap, result);
|
|
9954
|
+
const req = findingsToEvaluatedRequirement(controlId, controlMap.get(controlId), obsMap, riskMap, result, scanTime);
|
|
7953
9955
|
requirements.push(req);
|
|
7954
9956
|
}
|
|
7955
9957
|
const baseline = createMinimalBaseline(sarBaselineName(result, sar), requirements, {
|
|
@@ -7961,26 +9963,26 @@ async function resultToEvaluatedBaseline(result, sar, rawInput) {
|
|
|
7961
9963
|
if (result.description) baseline.description = result.description;
|
|
7962
9964
|
return baseline;
|
|
7963
9965
|
}
|
|
7964
|
-
function findingsToEvaluatedRequirement(controlId, findings, obsMap, riskMap, result) {
|
|
9966
|
+
function findingsToEvaluatedRequirement(controlId, findings, obsMap, riskMap, result, scanTime) {
|
|
7965
9967
|
const nistTag = controlIdToNistTag(controlId);
|
|
7966
9968
|
const title = findings[0].title || nistTag;
|
|
7967
9969
|
const impact = sarFindingsImpact(findings, riskMap);
|
|
7968
9970
|
const descriptions = sarBuildDescriptions(findings, obsMap);
|
|
7969
9971
|
const results = [];
|
|
7970
|
-
for (const f of findings) results.push(findingToRequirementResult(f, obsMap, riskMap, result));
|
|
9972
|
+
for (const f of findings) results.push(findingToRequirementResult(f, obsMap, riskMap, result, scanTime));
|
|
7971
9973
|
const req = createRequirement(nistTag, title, descriptions, impact, results, { tags: { nist: [nistTag] } });
|
|
7972
9974
|
const controlType = deriveControlTypeFromTags([nistTag]);
|
|
7973
9975
|
if (controlType !== void 0) req.controlType = controlType;
|
|
7974
9976
|
return req;
|
|
7975
9977
|
}
|
|
7976
|
-
function findingToRequirementResult(f, obsMap, riskMap, result) {
|
|
9978
|
+
function findingToRequirementResult(f, obsMap, riskMap, result, scanTime) {
|
|
7977
9979
|
const status = mapFindingStatus(f);
|
|
7978
9980
|
const codeDesc = buildCodeDesc(f, obsMap);
|
|
7979
9981
|
const message = buildRiskMessage(f, riskMap);
|
|
7980
9982
|
const startTime = parseResultStartTime(result);
|
|
7981
9983
|
return createResult(status, message || void 0, {
|
|
7982
9984
|
codeDesc,
|
|
7983
|
-
startTime: startTime.getTime() > 0 ? startTime :
|
|
9985
|
+
startTime: startTime.getTime() > 0 ? startTime : scanTime
|
|
7984
9986
|
});
|
|
7985
9987
|
}
|
|
7986
9988
|
function extractControlIdFromFinding(f) {
|
|
@@ -8075,8 +10077,8 @@ function buildRiskMap(risks) {
|
|
|
8075
10077
|
}
|
|
8076
10078
|
function parseResultStartTime(result) {
|
|
8077
10079
|
if (result.start) {
|
|
8078
|
-
const t =
|
|
8079
|
-
if (
|
|
10080
|
+
const t = parseTimestamp(String(result.start));
|
|
10081
|
+
if (t) return t;
|
|
8080
10082
|
}
|
|
8081
10083
|
return /* @__PURE__ */ new Date(0);
|
|
8082
10084
|
}
|
|
@@ -8084,6 +10086,6 @@ function sarBaselineName(result, sar) {
|
|
|
8084
10086
|
return toKebabCase(result.title || sar.metadata.title, "oscal-assessment-results");
|
|
8085
10087
|
}
|
|
8086
10088
|
//#endregion
|
|
8087
|
-
export { convertAwsConfigToHdf, convertBurpsuiteToHdf, convertCheckovToHdf, convertCklToHdf, convertCklbToHdf, convertConveyorToHdf, convertCyclonedxToHdf, convertDbprotectToHdf, convertDeptrackToHdf, convertFortifyToHdf, convertGitlabToHdf, convertGosecToHdf, convertGrypeToHdf, convertHdfToCkl, convertHdfToCklb, convertHdfToCsv, convertHdfToOscalPoam, convertHdfToOscalSar, convertHdfToXccdf, convertHdfToXml, convertIonchannelToHdf, convertJfrogXrayToHdf, convertJunitToHdf, convertMsftDefenderCloudToHdf, convertMsftDefenderDevopsToHdf, convertMsftDefenderEndpointToHdf, convertMsftSecureScoreToHdf, convertNessusToHdf, convertNetsparkerToHdf, convertNeuvectorToHdf, convertNiktoToHdf, convertOscalCatalogToHdf, convertOscalComponentToHdf, convertOscalPoamToHdf, convertOscalProfileToHdf, convertOscalSapToHdf, convertOscalSarToHdf, convertOscalSspToHdf, convertPrismaToHdf, convertSarifToHdf, convertScoutsuiteToHdf, convertSnykToHdf, convertSonarqubeToHdf, convertSplunkToHdf, convertTrufflehogToHdf, convertTwistlockToHdf, convertV1ToV2, convertVeracodeToHdf, convertXccdfResultsToHdf, convertZapToHdf, detectOscalDocumentType, isHDFV1 };
|
|
10089
|
+
export { convertAwsConfigToHdf, convertBurpsuiteToHdf, convertCheckovToHdf, convertCklToHdf, convertCklbToHdf, convertConveyorToHdf, convertCsafVexToHdf, convertCyclonedxToHdf, convertCyclonedxVexToHdf, convertDbprotectToHdf, convertDeptrackToHdf, convertFortifyToHdf, convertGitlabToHdf, convertGosecToHdf, convertGrypeToHdf, convertHdfToCkl, convertHdfToCklb, convertHdfToCsafVex, convertHdfToCsv, convertHdfToCyclonedxVex, convertHdfToOpenVex, convertHdfToOscalPoam, convertHdfToOscalSar, convertHdfToXccdf, convertHdfToXml, convertIonchannelToHdf, convertJfrogXrayToHdf, convertJunitToHdf, convertMsftDefenderCloudToHdf, convertMsftDefenderDevopsToHdf, convertMsftDefenderEndpointToHdf, convertMsftSecureScoreToHdf, convertNessusToHdf, convertNetsparkerToHdf, convertNeuvectorToHdf, convertNiktoToHdf, convertOpenVexToHdf, convertOscalCatalogToHdf, convertOscalComponentToHdf, convertOscalPoamToHdf, convertOscalProfileToHdf, convertOscalSapToHdf, convertOscalSarToHdf, convertOscalSspToHdf, convertPrismaToHdf, convertSarifToHdf, convertScoutsuiteToHdf, convertSnykToHdf, convertSonarqubeToHdf, convertSplunkToHdf, convertTrufflehogToHdf, convertTwistlockToHdf, convertV1ToV2, convertVeracodeToHdf, convertXccdfResultsToHdf, convertZapToHdf, detectOscalDocumentType, isHDFV1 };
|
|
8088
10090
|
|
|
8089
10091
|
//# sourceMappingURL=index.js.map
|