@mitre/hdf-converters 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -2
- package/dist/detect.d.ts.map +1 -1
- package/dist/detect.js +1 -1
- package/dist/index.d.ts +89 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3216 -223
- package/dist/index.js.map +1 -1
- package/dist/{register-all-DaYHszLd.js → register-all-C3lYICDC.js} +130 -9
- package/dist/register-all-C3lYICDC.js.map +1 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js.map +1 -1
- package/package.json +11 -8
- package/dist/register-all-DaYHszLd.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { n as detectConverter, t as registerAllFingerprints } from "./register-all-
|
|
1
|
+
import { n as detectConverter, t as registerAllFingerprints } from "./register-all-C3lYICDC.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 { AuthorizationStatus,
|
|
3
|
+
import { buildCsv, buildXml, cvssScoreToSeverity, parseCsv, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, sha256, stripHtml as stripHTML } 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
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";
|
|
6
6
|
//#region shared/typescript/converterutil.ts
|
|
7
7
|
/**
|
|
@@ -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)
|
|
@@ -202,6 +251,156 @@ function buildHdfResults(opts) {
|
|
|
202
251
|
if (opts.statistics) hdf.statistics = opts.statistics;
|
|
203
252
|
return JSON.stringify(hdf, null, 2);
|
|
204
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Captures a NIST 800-53 control identifier and its base sub-control number,
|
|
256
|
+
* ignoring enhancement suffixes like "(1)" or ".1". Mirrors the Go pattern
|
|
257
|
+
* in shared/go/converterutil.go.
|
|
258
|
+
*/
|
|
259
|
+
const NIST_TAG_PATTERN = /^([A-Z]{2})-(\d+)/;
|
|
260
|
+
/**
|
|
261
|
+
* Maps each recognized NIST 800-53 Rev 5 family to its HDF controlType per
|
|
262
|
+
* SP 800-53 Appendix C / SP 800-53A classification. Unknown families resolve
|
|
263
|
+
* to undefined.
|
|
264
|
+
*/
|
|
265
|
+
const NIST_FAMILY_CONTROL_TYPE = {
|
|
266
|
+
AC: ControlType.Technical,
|
|
267
|
+
AT: ControlType.Operational,
|
|
268
|
+
AU: ControlType.Operational,
|
|
269
|
+
CA: ControlType.Management,
|
|
270
|
+
CM: ControlType.Operational,
|
|
271
|
+
CP: ControlType.Operational,
|
|
272
|
+
IA: ControlType.Technical,
|
|
273
|
+
IR: ControlType.Operational,
|
|
274
|
+
MA: ControlType.Operational,
|
|
275
|
+
MP: ControlType.Operational,
|
|
276
|
+
PE: ControlType.Operational,
|
|
277
|
+
PL: ControlType.Management,
|
|
278
|
+
PM: ControlType.Management,
|
|
279
|
+
PS: ControlType.Operational,
|
|
280
|
+
PT: ControlType.Operational,
|
|
281
|
+
RA: ControlType.Management,
|
|
282
|
+
SA: ControlType.Management,
|
|
283
|
+
SC: ControlType.Technical,
|
|
284
|
+
SI: ControlType.Technical,
|
|
285
|
+
SR: ControlType.Management
|
|
286
|
+
};
|
|
287
|
+
/**
|
|
288
|
+
* Derive the HDF `controlType` for a single NIST 800-53 control identifier
|
|
289
|
+
* using a family-prefix heuristic. Returns undefined when the family is
|
|
290
|
+
* unrecognized or the tag is not a NIST control identifier.
|
|
291
|
+
*
|
|
292
|
+
* Heuristic, per NIST SP 800-53 Rev 5 Appendix C and SP 800-53A:
|
|
293
|
+
* - Management: PM, RA, CA, PL, SA, SR families
|
|
294
|
+
* - Operational: AT, AU, CM, CP, IR, MA, MP, PE, PS, PT families
|
|
295
|
+
* - Technical: AC, IA, SC, SI families
|
|
296
|
+
* - Policy: any "-1" sub-control (the per-family policy/procedure
|
|
297
|
+
* document, regardless of which family it belongs to). Enhancements of
|
|
298
|
+
* -1 controls (e.g., AC-1(1)) also resolve to policy.
|
|
299
|
+
*
|
|
300
|
+
* The "-1" rule takes precedence over the family rule because the per-family
|
|
301
|
+
* policy/procedure document is more usefully classified by its document
|
|
302
|
+
* nature than by the family it documents.
|
|
303
|
+
*
|
|
304
|
+
* @example
|
|
305
|
+
* deriveControlType("AC-3") // ControlType.Technical
|
|
306
|
+
* deriveControlType("AC-1") // ControlType.Policy (any *-1 is policy)
|
|
307
|
+
* deriveControlType("AC-3(1)") // ControlType.Technical (enhancement of AC-3)
|
|
308
|
+
* deriveControlType("PM-2") // ControlType.Management
|
|
309
|
+
* deriveControlType("SV-238196") // undefined (not a NIST tag)
|
|
310
|
+
*/
|
|
311
|
+
function deriveControlType(nistTag) {
|
|
312
|
+
const match = NIST_TAG_PATTERN.exec(nistTag.trim().toUpperCase());
|
|
313
|
+
if (!match || match[1] === void 0 || match[2] === void 0) return void 0;
|
|
314
|
+
const family = match[1];
|
|
315
|
+
const subControl = match[2];
|
|
316
|
+
const familyClass = NIST_FAMILY_CONTROL_TYPE[family];
|
|
317
|
+
if (familyClass === void 0) return void 0;
|
|
318
|
+
if (subControl === "1") return ControlType.Policy;
|
|
319
|
+
return familyClass;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* NIST tag sets this package uses as per-converter static fallbacks when no
|
|
323
|
+
* real per-finding mapping is available. When deriveControlTypeFromTags sees
|
|
324
|
+
* an input that exactly matches one of these bundles, it returns undefined —
|
|
325
|
+
* the input carries no real per-finding signal, and stamping every
|
|
326
|
+
* requirement with the same derived controlType is misleading.
|
|
327
|
+
*
|
|
328
|
+
* Keep in sync with DEFAULT_STATIC_ANALYSIS_NIST_TAGS, DEFAULT_REMEDIATION_NIST_TAGS.
|
|
329
|
+
*/
|
|
330
|
+
const NIST_FALLBACK_BUNDLES = [
|
|
331
|
+
["RA-5", "SA-11"],
|
|
332
|
+
["RA-5", "SI-2"],
|
|
333
|
+
["CM-8"]
|
|
334
|
+
];
|
|
335
|
+
function tagsMatchFallback(tags) {
|
|
336
|
+
if (tags.length === 0) return false;
|
|
337
|
+
const sorted = [...new Set(tags)].sort();
|
|
338
|
+
return NIST_FALLBACK_BUNDLES.some((bundle) => bundle.length === sorted.length && bundle.every((b, i) => b === sorted[i]));
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Derive the HDF `controlType` for a slice of NIST tags. Resolves each tag
|
|
342
|
+
* via {@link deriveControlType}, then picks the most-specific class by
|
|
343
|
+
* precedence: technical > operational > management > policy > procedure.
|
|
344
|
+
* The rationale is that a control enforced via configuration (technical)
|
|
345
|
+
* is more actionable than the same control's management/policy framing.
|
|
346
|
+
*
|
|
347
|
+
* Returns undefined when no tag resolves to a known classification, OR when
|
|
348
|
+
* the input exactly matches a converter-level static-fallback bundle (e.g.
|
|
349
|
+
* the DEFAULT_STATIC_ANALYSIS_NIST_TAGS set). The fallback gate prevents
|
|
350
|
+
* converters that have no per-finding NIST signal from stamping every
|
|
351
|
+
* requirement with the same misleading controlType.
|
|
352
|
+
*/
|
|
353
|
+
function deriveControlTypeFromTags(tags) {
|
|
354
|
+
if (tagsMatchFallback(tags)) return void 0;
|
|
355
|
+
const rank = {
|
|
356
|
+
[ControlType.Technical]: 0,
|
|
357
|
+
[ControlType.Operational]: 1,
|
|
358
|
+
[ControlType.Management]: 2,
|
|
359
|
+
[ControlType.Policy]: 3,
|
|
360
|
+
[ControlType.Procedure]: 4
|
|
361
|
+
};
|
|
362
|
+
let best;
|
|
363
|
+
let bestRank = Number.POSITIVE_INFINITY;
|
|
364
|
+
for (const tag of tags) {
|
|
365
|
+
const ct = deriveControlType(tag);
|
|
366
|
+
if (ct === void 0) continue;
|
|
367
|
+
const r = rank[ct];
|
|
368
|
+
if (r < bestRank) {
|
|
369
|
+
bestRank = r;
|
|
370
|
+
best = ct;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return best;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Derive the HDF `verificationMethod` for a requirement based on whether
|
|
377
|
+
* check code is present. Returns `automated` when code is non-empty (a check
|
|
378
|
+
* exists and runs without operator action), and undefined otherwise — the
|
|
379
|
+
* converter is responsible for distinguishing manual-by-design (statement-form,
|
|
380
|
+
* e.g. FedRAMP 20x KSI) from manual-pending-automation (a check that could
|
|
381
|
+
* be automated but isn't yet) when it has the source-format context to do so.
|
|
382
|
+
*/
|
|
383
|
+
function deriveVerificationMethod(code) {
|
|
384
|
+
if (code === void 0 || code === null || code === "") return void 0;
|
|
385
|
+
return VerificationMethodEnum.Automated;
|
|
386
|
+
}
|
|
387
|
+
function buildNoFindingsRequirement(id, codeDesc, startTime) {
|
|
388
|
+
return {
|
|
389
|
+
id,
|
|
390
|
+
title: "No findings reported",
|
|
391
|
+
impact: 0,
|
|
392
|
+
descriptions: [{
|
|
393
|
+
label: "default",
|
|
394
|
+
data: codeDesc
|
|
395
|
+
}],
|
|
396
|
+
results: [{
|
|
397
|
+
status: ResultStatus.Passed,
|
|
398
|
+
codeDesc,
|
|
399
|
+
startTime
|
|
400
|
+
}],
|
|
401
|
+
tags: {}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
205
404
|
//#endregion
|
|
206
405
|
//#region converters/legacyhdf-to-hdf/typescript/converter.ts
|
|
207
406
|
/**
|
|
@@ -247,7 +446,7 @@ function impactToSeverity$2(impact) {
|
|
|
247
446
|
* Normalize status values from v1.0 to v2.0 format.
|
|
248
447
|
* Converts snake_case to camelCase.
|
|
249
448
|
*/
|
|
250
|
-
function normalizeStatus(status) {
|
|
449
|
+
function normalizeStatus$1(status) {
|
|
251
450
|
return {
|
|
252
451
|
"passed": "passed",
|
|
253
452
|
"failed": "failed",
|
|
@@ -293,7 +492,7 @@ function computeEffectiveStatus(impact, results) {
|
|
|
293
492
|
* Transforms snake_case field names to camelCase.
|
|
294
493
|
*/
|
|
295
494
|
function convertResult(v1Result) {
|
|
296
|
-
const v2Result = { status: normalizeStatus(v1Result.status) };
|
|
495
|
+
const v2Result = { status: normalizeStatus$1(v1Result.status) };
|
|
297
496
|
if (v1Result.code_desc !== void 0) v2Result.codeDesc = v1Result.code_desc;
|
|
298
497
|
if (v1Result.run_time !== void 0) v2Result.runTime = v1Result.run_time;
|
|
299
498
|
if (v1Result.start_time !== void 0) v2Result.startTime = v1Result.start_time;
|
|
@@ -337,10 +536,15 @@ function convertControl(v1Control) {
|
|
|
337
536
|
if (v1Control.code !== void 0) v2Req.code = v1Control.code;
|
|
338
537
|
if (v1Control.source_location !== void 0) v2Req.sourceLocation = v1Control.source_location;
|
|
339
538
|
if (v1Control.waiver_data !== void 0) v2Req.waiverData = v1Control.waiver_data;
|
|
340
|
-
if (v1Control.status !== void 0) v2Req.effectiveStatus = normalizeStatus(v1Control.status);
|
|
539
|
+
if (v1Control.status !== void 0) v2Req.effectiveStatus = normalizeStatus$1(v1Control.status);
|
|
341
540
|
if (v1Control.results && Array.isArray(v1Control.results)) v2Req.results = v1Control.results.map(convertResult);
|
|
342
541
|
if (!v2Req.effectiveStatus) v2Req.effectiveStatus = computeEffectiveStatus(v1Control.impact, v2Req.results ?? []);
|
|
343
542
|
v2Req.severity = tagSeverityToSeverity(v1Control.tags?.severity) ?? impactToSeverity$2(v1Control.impact);
|
|
543
|
+
const rawNist = v1Control.tags?.nist;
|
|
544
|
+
if (Array.isArray(rawNist)) {
|
|
545
|
+
const controlType = deriveControlTypeFromTags(rawNist.filter((v) => typeof v === "string"));
|
|
546
|
+
if (controlType !== void 0) v2Req.controlType = controlType;
|
|
547
|
+
}
|
|
344
548
|
const knownFields = new Set([
|
|
345
549
|
"id",
|
|
346
550
|
"title",
|
|
@@ -544,18 +748,19 @@ async function convertSarifToHdf(input) {
|
|
|
544
748
|
const { items: limitedRuns, truncated: truncatedRuns } = limitArray(sarif.runs);
|
|
545
749
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
546
750
|
if (truncatedRuns) console.warn(`WARNING: Input truncated at ${limitedRuns.length} run items (original: ${sarif.runs.length})`);
|
|
751
|
+
const timestamp = /* @__PURE__ */ new Date();
|
|
547
752
|
return buildHdfResults({
|
|
548
753
|
generatorName: "sarif-to-hdf",
|
|
549
754
|
converterVersion: "1.0.0",
|
|
550
755
|
toolName: firstDriver?.name,
|
|
551
756
|
toolVersion: firstDriver?.version,
|
|
552
757
|
toolFormat: "SARIF",
|
|
553
|
-
baselines: limitedRuns.map((run) => convertRun(run, sarif.version, resultsChecksum)),
|
|
758
|
+
baselines: limitedRuns.map((run) => convertRun(run, sarif.version, resultsChecksum, timestamp)),
|
|
554
759
|
components: [],
|
|
555
|
-
timestamp
|
|
760
|
+
timestamp
|
|
556
761
|
});
|
|
557
762
|
}
|
|
558
|
-
function convertRun(run, version, resultsChecksum) {
|
|
763
|
+
function convertRun(run, version, resultsChecksum, timestamp) {
|
|
559
764
|
const ruleMap = buildRuleMap(run);
|
|
560
765
|
const { items: limitedResults, truncated: truncatedResults } = limitArray(run.results);
|
|
561
766
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
@@ -579,7 +784,14 @@ function convertRun(run, version, resultsChecksum) {
|
|
|
579
784
|
const group = groupMap.get(ruleId);
|
|
580
785
|
return convertResultGroup(ruleId, group.rule, group.results);
|
|
581
786
|
});
|
|
582
|
-
|
|
787
|
+
const baselineName = run.tool?.driver?.name || "SARIF";
|
|
788
|
+
if (requirements.length === 0) {
|
|
789
|
+
const driverName = run.tool?.driver?.name?.trim() || "";
|
|
790
|
+
const target = driverName || "SARIF analyzer";
|
|
791
|
+
const idPrefix = driverName || "sarif";
|
|
792
|
+
requirements.push(buildNoFindingsRequirement(`${idPrefix}-no-findings`, `${target} ran and reported zero findings.`, timestamp));
|
|
793
|
+
}
|
|
794
|
+
return createMinimalBaseline(baselineName, requirements, {
|
|
583
795
|
version,
|
|
584
796
|
title: "Static Analysis Results Interchange Format",
|
|
585
797
|
resultsChecksum
|
|
@@ -605,7 +817,45 @@ function convertResultGroup(ruleId, rule, sarifResults) {
|
|
|
605
817
|
const descriptions = buildDescriptions$1(description, rule, firstResult);
|
|
606
818
|
const options = { tags: buildTags$3(firstResult, rule, ruleLevel, cweIds, nistControls, cciControls) };
|
|
607
819
|
if (sourceLocation) options.sourceLocation = sourceLocation;
|
|
608
|
-
|
|
820
|
+
const req = createRequirement(ruleId, title, descriptions, impact, results, options);
|
|
821
|
+
const controlType = deriveControlTypeFromTags(nistControls);
|
|
822
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
823
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
824
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
825
|
+
const packages = [];
|
|
826
|
+
for (const sr of sarifResults) {
|
|
827
|
+
const pkg = packageFromSarifProperties(sr.properties);
|
|
828
|
+
if (!pkg) continue;
|
|
829
|
+
const key = pkg.purl ?? pkg.cpe ?? `${pkg.name ?? ""}@${pkg.version ?? ""}`;
|
|
830
|
+
if (seenKeys.has(key)) continue;
|
|
831
|
+
seenKeys.add(key);
|
|
832
|
+
packages.push(pkg);
|
|
833
|
+
}
|
|
834
|
+
if (packages.length > 0) req.affectedPackages = packages;
|
|
835
|
+
return req;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Extract an Affected_Package from a SARIF result.properties bag.
|
|
839
|
+
* Recognizes the SCA-tool convention of carrying purl / cpe /
|
|
840
|
+
* packageName+packageVersion / name+version. Returns undefined for
|
|
841
|
+
* SAST results that lack any package identity.
|
|
842
|
+
*/
|
|
843
|
+
function packageFromSarifProperties(props) {
|
|
844
|
+
if (!props) return void 0;
|
|
845
|
+
const name = props.packageName ?? props.name;
|
|
846
|
+
const version = props.packageVersion ?? props.version;
|
|
847
|
+
let ecosystem;
|
|
848
|
+
if (props.purl) ecosystem = ecosystemFromPurlType(parsePurl(props.purl)?.type);
|
|
849
|
+
else if (props.ecosystem) ecosystem = ecosystemFromPurlType(props.ecosystem);
|
|
850
|
+
else if (name && version) ecosystem = Ecosystem.Generic;
|
|
851
|
+
return buildAffectedPackage$1({
|
|
852
|
+
name,
|
|
853
|
+
version,
|
|
854
|
+
ecosystem,
|
|
855
|
+
purl: props.purl,
|
|
856
|
+
cpe: props.cpe,
|
|
857
|
+
fixedInVersion: props.fixedInVersion
|
|
858
|
+
});
|
|
609
859
|
}
|
|
610
860
|
function resolveRuleLevel(rule, results) {
|
|
611
861
|
if (rule?.defaultConfiguration?.level) return rule.defaultConfiguration.level;
|
|
@@ -810,8 +1060,8 @@ function createHDFResult(location, status, backtrace, suppressionMessage) {
|
|
|
810
1060
|
//#endregion
|
|
811
1061
|
//#region converters/junit-to-hdf/typescript/converter.ts
|
|
812
1062
|
const DEFAULT_NIST = ["SA-11"];
|
|
813
|
-
const CONVERTER_VERSION$
|
|
814
|
-
const ARRAY_TAGS$
|
|
1063
|
+
const CONVERTER_VERSION$2 = "1.0.0";
|
|
1064
|
+
const ARRAY_TAGS$2 = ["testsuite", "testcase"];
|
|
815
1065
|
/**
|
|
816
1066
|
* Converts JUnit XML test results to HDF format.
|
|
817
1067
|
*/
|
|
@@ -819,21 +1069,23 @@ async function convertJunitToHdf(input) {
|
|
|
819
1069
|
if (!input || !input.trim()) throw new Error("Empty input");
|
|
820
1070
|
validateInputSize(input, "junit");
|
|
821
1071
|
const { suites, name } = parseJUnitXML(input);
|
|
1072
|
+
const requirements = buildRequirements(suites);
|
|
1073
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("junit-no-findings", `JUnit scanned ${noFindingsTarget(name, suites)} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
822
1074
|
return buildHdfResults({
|
|
823
|
-
generatorName: "hdf
|
|
824
|
-
converterVersion: CONVERTER_VERSION$
|
|
1075
|
+
generatorName: "junit-to-hdf",
|
|
1076
|
+
converterVersion: CONVERTER_VERSION$2,
|
|
825
1077
|
toolName: "JUnit XML",
|
|
826
1078
|
toolFormat: "XML",
|
|
827
|
-
baselines: [createMinimalBaseline(name,
|
|
1079
|
+
baselines: [createMinimalBaseline(name, requirements, { resultsChecksum: await inputChecksum(input) })],
|
|
828
1080
|
components: [{
|
|
829
|
-
type:
|
|
1081
|
+
type: TargetType.Application,
|
|
830
1082
|
name
|
|
831
1083
|
}],
|
|
832
1084
|
timestamp: /* @__PURE__ */ new Date()
|
|
833
1085
|
});
|
|
834
1086
|
}
|
|
835
1087
|
function parseJUnitXML(input) {
|
|
836
|
-
const parsed = parseXmlWithArrays(input, ARRAY_TAGS$
|
|
1088
|
+
const parsed = parseXmlWithArrays(input, ARRAY_TAGS$2);
|
|
837
1089
|
if (parsed.testsuites) return {
|
|
838
1090
|
suites: parsed.testsuites.testsuite ?? [],
|
|
839
1091
|
name: parsed.testsuites.name || "JUnit Test Results"
|
|
@@ -876,7 +1128,11 @@ function testCaseToRequirement(tc, suiteTimestamp) {
|
|
|
876
1128
|
label: "default",
|
|
877
1129
|
data: `JUnit test: ${tc.name} in ${tc.classname || "unknown"}`
|
|
878
1130
|
}];
|
|
879
|
-
|
|
1131
|
+
const req = createRequirement(id, tc.name, descriptions, .5, [result], { tags: { nist: DEFAULT_NIST } });
|
|
1132
|
+
const controlType = deriveControlTypeFromTags(DEFAULT_NIST);
|
|
1133
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
1134
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
1135
|
+
return req;
|
|
880
1136
|
}
|
|
881
1137
|
function buildID(tc) {
|
|
882
1138
|
if (tc.classname) return `${tc.classname}.${tc.name}`;
|
|
@@ -921,12 +1177,17 @@ function buildCodeDesc$9(tc) {
|
|
|
921
1177
|
if (tc.classname) return `${tc.classname} :: ${tc.name}`;
|
|
922
1178
|
return tc.name;
|
|
923
1179
|
}
|
|
1180
|
+
function noFindingsTarget(baselineName, suites) {
|
|
1181
|
+
if (baselineName && baselineName !== "JUnit Test Results") return baselineName;
|
|
1182
|
+
for (const s of suites) if (s.name) return s.name;
|
|
1183
|
+
return "JUnit test suite";
|
|
1184
|
+
}
|
|
924
1185
|
//#endregion
|
|
925
1186
|
//#region converters/xccdf-results-to-hdf/typescript/converter.ts
|
|
926
|
-
const CONVERTER_VERSION = "1.0.0";
|
|
1187
|
+
const CONVERTER_VERSION$1 = "1.0.0";
|
|
927
1188
|
const CCI_SYSTEM = "http://cyber.mil/cci";
|
|
928
1189
|
/** Tags that must always be parsed as arrays even if only one element exists. */
|
|
929
|
-
const ARRAY_TAGS = [
|
|
1190
|
+
const ARRAY_TAGS$1 = [
|
|
930
1191
|
"Group",
|
|
931
1192
|
"Rule",
|
|
932
1193
|
"Profile",
|
|
@@ -969,7 +1230,7 @@ const STATUS_MAP = {
|
|
|
969
1230
|
async function convertXccdfResultsToHdf(input) {
|
|
970
1231
|
if (!input || !input.trim()) throw new Error("Empty input");
|
|
971
1232
|
validateInputSize(input, "xccdf-results");
|
|
972
|
-
const parsed = parseXmlWithArrays(input, ARRAY_TAGS);
|
|
1233
|
+
const parsed = parseXmlWithArrays(input, ARRAY_TAGS$1);
|
|
973
1234
|
const arfParsed = parsed;
|
|
974
1235
|
if (arfParsed["asset-report-collection"]) return convertArfCollection(arfParsed["asset-report-collection"], input);
|
|
975
1236
|
const benchmark = parsed.Benchmark;
|
|
@@ -985,6 +1246,10 @@ async function convertBenchmarkResultsToHdf(benchmark, rawInput) {
|
|
|
985
1246
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
986
1247
|
if (truncatedRR) console.warn(`WARNING: Input truncated at ${limitedRuleResults.length} rule-result items (original: ${ruleResults.length})`);
|
|
987
1248
|
const requirements = limitedRuleResults.map((rr) => ruleResultToRequirement(rr, ruleIndex));
|
|
1249
|
+
if (requirements.length === 0) {
|
|
1250
|
+
const target = xccdfTargetName(testResult, benchmark);
|
|
1251
|
+
requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
1252
|
+
}
|
|
988
1253
|
const resultsChecksum = await inputChecksum(rawInput);
|
|
989
1254
|
const baseline = createMinimalBaseline(extractText(benchmark.title) || "XCCDF Benchmark", requirements, { resultsChecksum });
|
|
990
1255
|
const components = buildTargets(testResult);
|
|
@@ -998,8 +1263,8 @@ async function convertBenchmarkResultsToHdf(benchmark, rawInput) {
|
|
|
998
1263
|
const hdf = {
|
|
999
1264
|
baselines: [baseline],
|
|
1000
1265
|
generator: {
|
|
1001
|
-
name: "hdf
|
|
1002
|
-
version: CONVERTER_VERSION
|
|
1266
|
+
name: "xccdf-results-to-hdf",
|
|
1267
|
+
version: CONVERTER_VERSION$1
|
|
1003
1268
|
},
|
|
1004
1269
|
tool: {
|
|
1005
1270
|
name: "XCCDF Results",
|
|
@@ -1037,6 +1302,10 @@ async function convertArfCollection(arc, rawInput) {
|
|
|
1037
1302
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
1038
1303
|
if (truncatedARFRR) console.warn(`WARNING: Input truncated at ${limitedARFRuleResults.length} rule-result items (original: ${ruleResults.length})`);
|
|
1039
1304
|
const requirements = limitedARFRuleResults.map((rr) => ruleResultToRequirement(rr, ruleIndex));
|
|
1305
|
+
if (requirements.length === 0) {
|
|
1306
|
+
const target = xccdfTargetName(testResult, benchmark);
|
|
1307
|
+
requirements.push(buildNoFindingsRequirement("xccdf-results-no-findings", `XCCDF scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
1308
|
+
}
|
|
1040
1309
|
let baselineName = "";
|
|
1041
1310
|
if (benchmark) baselineName = extractText(benchmark.title) || "";
|
|
1042
1311
|
if (!baselineName) baselineName = extractText(testResult.title) || testResult.id || "ARF Report";
|
|
@@ -1057,8 +1326,8 @@ async function convertArfCollection(arc, rawInput) {
|
|
|
1057
1326
|
const hdf = {
|
|
1058
1327
|
baselines,
|
|
1059
1328
|
generator: {
|
|
1060
|
-
name: "hdf
|
|
1061
|
-
version: CONVERTER_VERSION
|
|
1329
|
+
name: "xccdf-results-to-hdf",
|
|
1330
|
+
version: CONVERTER_VERSION$1
|
|
1062
1331
|
},
|
|
1063
1332
|
tool: {
|
|
1064
1333
|
name: "ARF",
|
|
@@ -1144,13 +1413,33 @@ function ruleResultToRequirement(rr, ruleIndex) {
|
|
|
1144
1413
|
});
|
|
1145
1414
|
const result = createResult(STATUS_MAP[(rr.result ?? "").toLowerCase()] ?? ResultStatus.NotReviewed, "", { codeDesc: `XCCDF rule ${id}` });
|
|
1146
1415
|
const tags = {};
|
|
1416
|
+
let nistTags = [];
|
|
1147
1417
|
const cciIds = extractCCIs(rr.ident ?? ruleDef?.ident ?? []);
|
|
1148
1418
|
if (cciIds.length > 0) {
|
|
1149
1419
|
tags["cci"] = cciIds;
|
|
1150
|
-
|
|
1151
|
-
if (nistTags.length > 0) tags["nist"] =
|
|
1420
|
+
nistTags = [...new Set(cciIds.flatMap((cci) => getCCINistMappings(cci) ?? []))];
|
|
1421
|
+
if (nistTags.length > 0) tags["nist"] = nistTags;
|
|
1152
1422
|
}
|
|
1153
|
-
|
|
1423
|
+
const req = createRequirement(id, title, descriptions, impact, [result], { tags });
|
|
1424
|
+
const controlType = deriveControlTypeFromTags(nistTags);
|
|
1425
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
1426
|
+
return req;
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Pick the most specific identifier available for a no-findings codeDesc.
|
|
1430
|
+
* Falls back through TestResult target/title, benchmark title/id, then a generic phrase.
|
|
1431
|
+
*/
|
|
1432
|
+
function xccdfTargetName(testResult, benchmark) {
|
|
1433
|
+
const tr = testResult ?? {};
|
|
1434
|
+
const target = (tr.target ?? "").trim();
|
|
1435
|
+
if (target) return target;
|
|
1436
|
+
const trTitle = extractText(tr.title).trim();
|
|
1437
|
+
if (trTitle) return trTitle;
|
|
1438
|
+
const benchTitle = extractText(benchmark?.title).trim();
|
|
1439
|
+
if (benchTitle) return benchTitle;
|
|
1440
|
+
const benchId = (benchmark?.id ?? "").trim();
|
|
1441
|
+
if (benchId) return benchId;
|
|
1442
|
+
return "the target";
|
|
1154
1443
|
}
|
|
1155
1444
|
/**
|
|
1156
1445
|
* Build Component array from TestResult metadata.
|
|
@@ -1161,7 +1450,7 @@ function buildTargets(testResult) {
|
|
|
1161
1450
|
const addresses = testResult["target-address"] ?? [];
|
|
1162
1451
|
const target = {
|
|
1163
1452
|
name: targetName,
|
|
1164
|
-
type:
|
|
1453
|
+
type: TargetType.Host,
|
|
1165
1454
|
labels: {}
|
|
1166
1455
|
};
|
|
1167
1456
|
if (addresses.length > 0) target.ipAddress = addresses[0];
|
|
@@ -1210,6 +1499,713 @@ function extractCCIs(idents) {
|
|
|
1210
1499
|
return idents.filter((i) => i.system === CCI_SYSTEM).map((i) => i["#text"] ?? "").filter((v) => v.length > 0);
|
|
1211
1500
|
}
|
|
1212
1501
|
//#endregion
|
|
1502
|
+
//#region shared/typescript/checklist/status.ts
|
|
1503
|
+
function normalizeKey(s) {
|
|
1504
|
+
return s.toLowerCase().trim().split("_").join("").split(" ").join("");
|
|
1505
|
+
}
|
|
1506
|
+
const STATUS_BY_KEY = {
|
|
1507
|
+
open: "Open",
|
|
1508
|
+
notafinding: "NotAFinding",
|
|
1509
|
+
notreviewed: "Not_Reviewed",
|
|
1510
|
+
notapplicable: "Not_Applicable"
|
|
1511
|
+
};
|
|
1512
|
+
/** Map any CKL/CKLB status spelling to the canonical CheckStatus. */
|
|
1513
|
+
function parseStatus(s) {
|
|
1514
|
+
return STATUS_BY_KEY[normalizeKey(s ?? "")] ?? "Not_Reviewed";
|
|
1515
|
+
}
|
|
1516
|
+
/** CKL (XML) spelling. */
|
|
1517
|
+
function statusToCkl(s) {
|
|
1518
|
+
return s || "Not_Reviewed";
|
|
1519
|
+
}
|
|
1520
|
+
/** CKLB (JSON) snake_case spelling. */
|
|
1521
|
+
function statusToCklb(s) {
|
|
1522
|
+
switch (s) {
|
|
1523
|
+
case "Open": return "open";
|
|
1524
|
+
case "NotAFinding": return "not_a_finding";
|
|
1525
|
+
case "Not_Applicable": return "not_applicable";
|
|
1526
|
+
default: return "not_reviewed";
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
/** Canonical status -> HDF Result_Status. */
|
|
1530
|
+
function statusToHdf(s) {
|
|
1531
|
+
switch (s) {
|
|
1532
|
+
case "Open": return ResultStatus.Failed;
|
|
1533
|
+
case "NotAFinding": return ResultStatus.Passed;
|
|
1534
|
+
case "Not_Applicable": return ResultStatus.NotApplicable;
|
|
1535
|
+
default: return ResultStatus.NotReviewed;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
/** HDF Result_Status -> canonical status (error -> Open). */
|
|
1539
|
+
function statusFromHdf(s) {
|
|
1540
|
+
switch (s) {
|
|
1541
|
+
case ResultStatus.Passed: return "NotAFinding";
|
|
1542
|
+
case ResultStatus.Failed:
|
|
1543
|
+
case ResultStatus.Error: return "Open";
|
|
1544
|
+
case ResultStatus.NotApplicable: return "Not_Applicable";
|
|
1545
|
+
default: return "Not_Reviewed";
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
//#endregion
|
|
1549
|
+
//#region shared/typescript/checklist/ckl.ts
|
|
1550
|
+
const ARRAY_TAGS = [
|
|
1551
|
+
"iSTIG",
|
|
1552
|
+
"VULN",
|
|
1553
|
+
"STIG_DATA",
|
|
1554
|
+
"SI_DATA"
|
|
1555
|
+
];
|
|
1556
|
+
const BUILD_OPTIONS$1 = {
|
|
1557
|
+
attributeNamePrefix: "@_",
|
|
1558
|
+
textNodeName: "#text",
|
|
1559
|
+
ignoreAttributes: false,
|
|
1560
|
+
format: true,
|
|
1561
|
+
indentBy: " ",
|
|
1562
|
+
suppressEmptyNode: false
|
|
1563
|
+
};
|
|
1564
|
+
function str(v) {
|
|
1565
|
+
return v === void 0 || v === null ? "" : String(v);
|
|
1566
|
+
}
|
|
1567
|
+
const VULN_ATTR_ORDER = [
|
|
1568
|
+
"Vuln_Num",
|
|
1569
|
+
"Severity",
|
|
1570
|
+
"Group_Title",
|
|
1571
|
+
"Rule_ID",
|
|
1572
|
+
"Rule_Ver",
|
|
1573
|
+
"Rule_Title",
|
|
1574
|
+
"Vuln_Discuss",
|
|
1575
|
+
"IA_Controls",
|
|
1576
|
+
"Check_Content",
|
|
1577
|
+
"Fix_Text",
|
|
1578
|
+
"False_Positives",
|
|
1579
|
+
"False_Negatives",
|
|
1580
|
+
"Documentable",
|
|
1581
|
+
"Mitigations",
|
|
1582
|
+
"Potential_Impact",
|
|
1583
|
+
"Third_Party_Tools",
|
|
1584
|
+
"Mitigation_Control",
|
|
1585
|
+
"Responsibility",
|
|
1586
|
+
"Security_Override_Guidance",
|
|
1587
|
+
"Check_Content_Ref",
|
|
1588
|
+
"Weight",
|
|
1589
|
+
"Class",
|
|
1590
|
+
"STIG_UUID"
|
|
1591
|
+
];
|
|
1592
|
+
const CORE_VULN_ATTR = new Set([
|
|
1593
|
+
"Vuln_Num",
|
|
1594
|
+
"Severity",
|
|
1595
|
+
"Group_Title",
|
|
1596
|
+
"Rule_ID",
|
|
1597
|
+
"Rule_Ver",
|
|
1598
|
+
"Rule_Title",
|
|
1599
|
+
"Vuln_Discuss",
|
|
1600
|
+
"Check_Content",
|
|
1601
|
+
"Fix_Text",
|
|
1602
|
+
"Weight",
|
|
1603
|
+
"Class"
|
|
1604
|
+
]);
|
|
1605
|
+
/** Parse CKL XML into the format-neutral Checklist model. */
|
|
1606
|
+
function parseCkl(input) {
|
|
1607
|
+
const checklist = parseXmlWithArrays(input, ARRAY_TAGS, { processEntities: true }).CHECKLIST;
|
|
1608
|
+
const istigs = checklist?.STIGS?.iSTIG;
|
|
1609
|
+
if (!checklist || !istigs || istigs.length === 0) throw new Error("parse ckl: no <iSTIG> blocks found (not a CKL document?)");
|
|
1610
|
+
const a = checklist.ASSET ?? {};
|
|
1611
|
+
return {
|
|
1612
|
+
format: "ckl",
|
|
1613
|
+
asset: {
|
|
1614
|
+
role: str(a["ROLE"]),
|
|
1615
|
+
assetType: str(a["ASSET_TYPE"]),
|
|
1616
|
+
marking: str(a["MARKING"]),
|
|
1617
|
+
hostName: str(a["HOST_NAME"]),
|
|
1618
|
+
hostIP: str(a["HOST_IP"]),
|
|
1619
|
+
hostMAC: str(a["HOST_MAC"]),
|
|
1620
|
+
hostFQDN: str(a["HOST_FQDN"]),
|
|
1621
|
+
targetComment: str(a["TARGET_COMMENT"]),
|
|
1622
|
+
techArea: str(a["TECH_AREA"]),
|
|
1623
|
+
targetKey: str(a["TARGET_KEY"]),
|
|
1624
|
+
webOrDatabase: str(a["WEB_OR_DATABASE"]) === "true",
|
|
1625
|
+
webDBSite: str(a["WEB_DB_SITE"]),
|
|
1626
|
+
webDBInstance: str(a["WEB_DB_INSTANCE"])
|
|
1627
|
+
},
|
|
1628
|
+
stigs: istigs.map((is) => {
|
|
1629
|
+
const si = is.STIG_INFO?.SI_DATA ?? [];
|
|
1630
|
+
const siVal = (name) => str(si.find((d) => d.SID_NAME === name)?.SID_DATA);
|
|
1631
|
+
return {
|
|
1632
|
+
stigID: siVal("stigid"),
|
|
1633
|
+
title: siVal("title"),
|
|
1634
|
+
version: siVal("version"),
|
|
1635
|
+
releaseInfo: siVal("releaseinfo"),
|
|
1636
|
+
uuid: siVal("uuid"),
|
|
1637
|
+
classification: siVal("classification"),
|
|
1638
|
+
vulns: (is.VULN ?? []).map(cklVulnToModel)
|
|
1639
|
+
};
|
|
1640
|
+
})
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
function cklVulnToModel(v) {
|
|
1644
|
+
const data = v.STIG_DATA ?? [];
|
|
1645
|
+
const attr = (name) => str(data.find((sd) => sd.VULN_ATTRIBUTE === name)?.ATTRIBUTE_DATA);
|
|
1646
|
+
const ccis = [];
|
|
1647
|
+
const extra = {};
|
|
1648
|
+
const promoted = new Set([
|
|
1649
|
+
"CCI_REF",
|
|
1650
|
+
"Vuln_Num",
|
|
1651
|
+
"Severity",
|
|
1652
|
+
"Group_Title",
|
|
1653
|
+
"Rule_ID",
|
|
1654
|
+
"Rule_Ver",
|
|
1655
|
+
"Rule_Title",
|
|
1656
|
+
"Vuln_Discuss",
|
|
1657
|
+
"Check_Content",
|
|
1658
|
+
"Fix_Text",
|
|
1659
|
+
"Weight",
|
|
1660
|
+
"Class"
|
|
1661
|
+
]);
|
|
1662
|
+
for (const sd of data) {
|
|
1663
|
+
const name = sd.VULN_ATTRIBUTE ?? "";
|
|
1664
|
+
const val = str(sd.ATTRIBUTE_DATA);
|
|
1665
|
+
if (name === "CCI_REF") {
|
|
1666
|
+
if (val) ccis.push(val);
|
|
1667
|
+
} else if (!promoted.has(name) && val) extra[name] = val;
|
|
1668
|
+
}
|
|
1669
|
+
return {
|
|
1670
|
+
vulnNum: attr("Vuln_Num"),
|
|
1671
|
+
ruleID: attr("Rule_ID"),
|
|
1672
|
+
ruleVer: attr("Rule_Ver"),
|
|
1673
|
+
groupTitle: attr("Group_Title"),
|
|
1674
|
+
severity: attr("Severity"),
|
|
1675
|
+
ruleTitle: attr("Rule_Title"),
|
|
1676
|
+
vulnDiscuss: attr("Vuln_Discuss"),
|
|
1677
|
+
checkContent: attr("Check_Content"),
|
|
1678
|
+
fixText: attr("Fix_Text"),
|
|
1679
|
+
weight: attr("Weight"),
|
|
1680
|
+
classification: attr("Class"),
|
|
1681
|
+
ccis,
|
|
1682
|
+
status: parseStatus(v.STATUS),
|
|
1683
|
+
findingDetails: str(v.FINDING_DETAILS),
|
|
1684
|
+
comments: str(v.COMMENTS),
|
|
1685
|
+
extra
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
/** Serialize the Checklist model to CKL XML (with declaration). */
|
|
1689
|
+
function serializeCkl(cl) {
|
|
1690
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n${buildXml({ CHECKLIST: {
|
|
1691
|
+
ASSET: {
|
|
1692
|
+
ROLE: cl.asset.role || "None",
|
|
1693
|
+
ASSET_TYPE: cl.asset.assetType || "Computing",
|
|
1694
|
+
HOST_NAME: cl.asset.hostName ?? "",
|
|
1695
|
+
HOST_IP: cl.asset.hostIP ?? "",
|
|
1696
|
+
HOST_MAC: cl.asset.hostMAC ?? "",
|
|
1697
|
+
HOST_FQDN: cl.asset.hostFQDN ?? "",
|
|
1698
|
+
TARGET_COMMENT: cl.asset.targetComment ?? "",
|
|
1699
|
+
TECH_AREA: cl.asset.techArea ?? "",
|
|
1700
|
+
TARGET_KEY: cl.asset.targetKey ?? "",
|
|
1701
|
+
WEB_OR_DATABASE: cl.asset.webOrDatabase ? "true" : "false",
|
|
1702
|
+
WEB_DB_SITE: cl.asset.webDBSite ?? "",
|
|
1703
|
+
WEB_DB_INSTANCE: cl.asset.webDBInstance ?? ""
|
|
1704
|
+
},
|
|
1705
|
+
STIGS: { iSTIG: cl.stigs.map((s) => ({
|
|
1706
|
+
STIG_INFO: { SI_DATA: stigInfoSiData(s) },
|
|
1707
|
+
VULN: s.vulns.map(modelVulnToCkl)
|
|
1708
|
+
})) }
|
|
1709
|
+
} }, BUILD_OPTIONS$1)}`;
|
|
1710
|
+
}
|
|
1711
|
+
function stigInfoSiData(s) {
|
|
1712
|
+
return [
|
|
1713
|
+
{
|
|
1714
|
+
name: "version",
|
|
1715
|
+
data: s.version
|
|
1716
|
+
},
|
|
1717
|
+
{
|
|
1718
|
+
name: "classification",
|
|
1719
|
+
data: s.classification
|
|
1720
|
+
},
|
|
1721
|
+
{
|
|
1722
|
+
name: "stigid",
|
|
1723
|
+
data: s.stigID
|
|
1724
|
+
},
|
|
1725
|
+
{
|
|
1726
|
+
name: "releaseinfo",
|
|
1727
|
+
data: s.releaseInfo
|
|
1728
|
+
},
|
|
1729
|
+
{
|
|
1730
|
+
name: "title",
|
|
1731
|
+
data: s.title
|
|
1732
|
+
},
|
|
1733
|
+
{
|
|
1734
|
+
name: "uuid",
|
|
1735
|
+
data: s.uuid
|
|
1736
|
+
}
|
|
1737
|
+
].filter((p) => p.data).map((p) => ({
|
|
1738
|
+
SID_NAME: p.name,
|
|
1739
|
+
SID_DATA: p.data
|
|
1740
|
+
}));
|
|
1741
|
+
}
|
|
1742
|
+
function modelVulnToCkl(v) {
|
|
1743
|
+
const typed = {
|
|
1744
|
+
Vuln_Num: v.vulnNum,
|
|
1745
|
+
Severity: v.severity ?? "",
|
|
1746
|
+
Group_Title: v.groupTitle ?? "",
|
|
1747
|
+
Rule_ID: v.ruleID ?? "",
|
|
1748
|
+
Rule_Ver: v.ruleVer ?? "",
|
|
1749
|
+
Rule_Title: v.ruleTitle ?? "",
|
|
1750
|
+
Vuln_Discuss: v.vulnDiscuss ?? "",
|
|
1751
|
+
Check_Content: v.checkContent ?? "",
|
|
1752
|
+
Fix_Text: v.fixText ?? "",
|
|
1753
|
+
Weight: v.weight || "10.0",
|
|
1754
|
+
Class: v.classification || "Unclass"
|
|
1755
|
+
};
|
|
1756
|
+
const stigData = [];
|
|
1757
|
+
for (const name of VULN_ATTR_ORDER) {
|
|
1758
|
+
const val = typed[name] ?? v.extra?.[name] ?? "";
|
|
1759
|
+
if (!val && !CORE_VULN_ATTR.has(name)) continue;
|
|
1760
|
+
stigData.push({
|
|
1761
|
+
VULN_ATTRIBUTE: name,
|
|
1762
|
+
ATTRIBUTE_DATA: val
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
for (const cci of v.ccis) stigData.push({
|
|
1766
|
+
VULN_ATTRIBUTE: "CCI_REF",
|
|
1767
|
+
ATTRIBUTE_DATA: cci
|
|
1768
|
+
});
|
|
1769
|
+
return {
|
|
1770
|
+
STIG_DATA: stigData,
|
|
1771
|
+
STATUS: statusToCkl(v.status),
|
|
1772
|
+
FINDING_DETAILS: v.findingDetails ?? "",
|
|
1773
|
+
COMMENTS: v.comments ?? ""
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
//#endregion
|
|
1777
|
+
//#region shared/typescript/checklist/cklb.ts
|
|
1778
|
+
/** Parse CKLB JSON into the format-neutral Checklist model. */
|
|
1779
|
+
function parseCklb(input) {
|
|
1780
|
+
const doc = parseJSON(input);
|
|
1781
|
+
if (!doc || !Array.isArray(doc.stigs) || doc.stigs.length === 0) throw new Error("parse cklb: no stigs[] found (not a CKLB document?)");
|
|
1782
|
+
const t = doc.target_data ?? {};
|
|
1783
|
+
const asset = {
|
|
1784
|
+
role: nz(t.role),
|
|
1785
|
+
assetType: nz(t.target_type),
|
|
1786
|
+
hostName: nz(t.host_name),
|
|
1787
|
+
hostIP: nz(t.ip_address),
|
|
1788
|
+
hostMAC: nz(t.mac_address),
|
|
1789
|
+
hostFQDN: nz(t.fqdn),
|
|
1790
|
+
targetComment: nz(t.comments),
|
|
1791
|
+
webOrDatabase: t.is_web_database === true,
|
|
1792
|
+
webDBSite: nz(t.web_db_site),
|
|
1793
|
+
webDBInstance: nz(t.web_db_instance),
|
|
1794
|
+
techArea: nz(t.technology_area),
|
|
1795
|
+
classification: nz(t.classification)
|
|
1796
|
+
};
|
|
1797
|
+
const stigs = doc.stigs.map((s) => ({
|
|
1798
|
+
stigID: nz(s.stig_id),
|
|
1799
|
+
title: nz(s.stig_name) || nz(s.display_name),
|
|
1800
|
+
displayName: nz(s.display_name),
|
|
1801
|
+
version: nz(s.version),
|
|
1802
|
+
releaseInfo: nz(s.release_info),
|
|
1803
|
+
uuid: nz(s.uuid),
|
|
1804
|
+
referenceIdentifier: nz(s.reference_identifier),
|
|
1805
|
+
vulns: (s.rules ?? []).map(cklbRuleToModel)
|
|
1806
|
+
}));
|
|
1807
|
+
return {
|
|
1808
|
+
format: "cklb",
|
|
1809
|
+
cklbVersion: nz(doc.cklb_version),
|
|
1810
|
+
asset,
|
|
1811
|
+
stigs
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
/** Coerce a possibly-null/non-string JSON value to string | undefined.
|
|
1815
|
+
* Real STIG Viewer CKLB output uses null for unset fields (e.g.
|
|
1816
|
+
* target_data.classification: null); the model expects string | undefined. */
|
|
1817
|
+
function nz(v) {
|
|
1818
|
+
return typeof v === "string" ? v : void 0;
|
|
1819
|
+
}
|
|
1820
|
+
function cklbRuleToModel(r) {
|
|
1821
|
+
const extra = {};
|
|
1822
|
+
if (r.third_party_tools) extra["Third_Party_Tools"] = r.third_party_tools;
|
|
1823
|
+
if (r.srg_id) extra["SRG_ID"] = r.srg_id;
|
|
1824
|
+
return {
|
|
1825
|
+
vulnNum: nz(r.group_id) ?? "",
|
|
1826
|
+
ruleID: nz(r.rule_id),
|
|
1827
|
+
ruleVer: nz(r.rule_version),
|
|
1828
|
+
groupID: nz(r.group_id),
|
|
1829
|
+
groupTitle: nz(r.group_title),
|
|
1830
|
+
severity: nz(r.severity),
|
|
1831
|
+
ruleTitle: nz(r.rule_title),
|
|
1832
|
+
vulnDiscuss: nz(r.discussion),
|
|
1833
|
+
checkContent: nz(r.check_content),
|
|
1834
|
+
fixText: nz(r.fix_text),
|
|
1835
|
+
weight: nz(r.weight),
|
|
1836
|
+
classification: nz(r.classification),
|
|
1837
|
+
ccis: Array.isArray(r.ccis) ? r.ccis.filter((c) => typeof c === "string") : [],
|
|
1838
|
+
legacyIDs: r.legacy_ids,
|
|
1839
|
+
status: parseStatus(r.status),
|
|
1840
|
+
findingDetails: nz(r.finding_details),
|
|
1841
|
+
comments: nz(r.comments),
|
|
1842
|
+
extra
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
/** Serialize the Checklist model to CKLB JSON. */
|
|
1846
|
+
function serializeCklb(cl) {
|
|
1847
|
+
const doc = {
|
|
1848
|
+
title: cklbTitle(cl),
|
|
1849
|
+
cklb_version: cl.cklbVersion || "1.0",
|
|
1850
|
+
active: false,
|
|
1851
|
+
has_path: false,
|
|
1852
|
+
target_data: {
|
|
1853
|
+
target_type: cl.asset.assetType || "Computing",
|
|
1854
|
+
host_name: cl.asset.hostName ?? "",
|
|
1855
|
+
ip_address: cl.asset.hostIP ?? "",
|
|
1856
|
+
mac_address: cl.asset.hostMAC ?? "",
|
|
1857
|
+
fqdn: cl.asset.hostFQDN ?? "",
|
|
1858
|
+
comments: cl.asset.targetComment ?? "",
|
|
1859
|
+
role: cl.asset.role || "None",
|
|
1860
|
+
is_web_database: cl.asset.webOrDatabase ?? false,
|
|
1861
|
+
technology_area: cl.asset.techArea ?? "",
|
|
1862
|
+
web_db_site: cl.asset.webDBSite ?? "",
|
|
1863
|
+
web_db_instance: cl.asset.webDBInstance ?? "",
|
|
1864
|
+
...cl.asset.classification ? { classification: cl.asset.classification } : {}
|
|
1865
|
+
},
|
|
1866
|
+
stigs: cl.stigs.map((s) => ({
|
|
1867
|
+
stig_name: s.title ?? "",
|
|
1868
|
+
display_name: s.displayName || s.title || "",
|
|
1869
|
+
stig_id: s.stigID ?? "",
|
|
1870
|
+
release_info: s.releaseInfo ?? "",
|
|
1871
|
+
version: s.version ?? "",
|
|
1872
|
+
uuid: s.uuid ?? "",
|
|
1873
|
+
...s.referenceIdentifier ? { reference_identifier: s.referenceIdentifier } : {},
|
|
1874
|
+
rules: s.vulns.map((v) => ({
|
|
1875
|
+
group_id: v.groupID || v.vulnNum,
|
|
1876
|
+
group_title: v.groupTitle ?? "",
|
|
1877
|
+
rule_id: v.ruleID ?? "",
|
|
1878
|
+
rule_version: v.ruleVer ?? "",
|
|
1879
|
+
rule_title: v.ruleTitle ?? "",
|
|
1880
|
+
severity: v.severity ?? "",
|
|
1881
|
+
weight: v.weight || "10.0",
|
|
1882
|
+
check_content: v.checkContent ?? "",
|
|
1883
|
+
fix_text: v.fixText ?? "",
|
|
1884
|
+
discussion: v.vulnDiscuss ?? "",
|
|
1885
|
+
ccis: v.ccis,
|
|
1886
|
+
...v.legacyIDs && v.legacyIDs.length ? { legacy_ids: v.legacyIDs } : {},
|
|
1887
|
+
status: statusToCklb(v.status),
|
|
1888
|
+
comments: v.comments ?? "",
|
|
1889
|
+
finding_details: v.findingDetails ?? "",
|
|
1890
|
+
...v.extra?.["Third_Party_Tools"] ? { third_party_tools: v.extra["Third_Party_Tools"] } : {}
|
|
1891
|
+
}))
|
|
1892
|
+
}))
|
|
1893
|
+
};
|
|
1894
|
+
return JSON.stringify(doc, null, 2);
|
|
1895
|
+
}
|
|
1896
|
+
function cklbTitle(cl) {
|
|
1897
|
+
return cl.stigs[0]?.title || "STIG Checklist";
|
|
1898
|
+
}
|
|
1899
|
+
//#endregion
|
|
1900
|
+
//#region shared/typescript/checklist/to-hdf.ts
|
|
1901
|
+
const CONVERTER_VERSION = "1.0.0";
|
|
1902
|
+
/**
|
|
1903
|
+
* Map the format-neutral Checklist model to an HDF Results object.
|
|
1904
|
+
* controlType is derived per-Vuln from CCI->NIST; verificationMethod and
|
|
1905
|
+
* applicability are omitted (the checklist format cannot substantiate them).
|
|
1906
|
+
* Original-format metadata is stashed in extensions/tags for round-trip.
|
|
1907
|
+
*/
|
|
1908
|
+
function checklistToHdf(cl, resultsChecksum, generatorName) {
|
|
1909
|
+
const hdf = {
|
|
1910
|
+
baselines: cl.stigs.map((s) => stigToBaseline(s, resultsChecksum)),
|
|
1911
|
+
generator: {
|
|
1912
|
+
name: generatorName,
|
|
1913
|
+
version: CONVERTER_VERSION
|
|
1914
|
+
},
|
|
1915
|
+
tool: {
|
|
1916
|
+
name: "DISA STIG Viewer",
|
|
1917
|
+
format: cl.format === "cklb" ? "CKLB" : "CKL"
|
|
1918
|
+
},
|
|
1919
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1920
|
+
};
|
|
1921
|
+
const component = assetToComponent(cl.asset);
|
|
1922
|
+
if (component) hdf.components = [component];
|
|
1923
|
+
const ext = rootExtensions(cl);
|
|
1924
|
+
if (Object.keys(ext).length > 0) hdf.extensions = ext;
|
|
1925
|
+
return hdf;
|
|
1926
|
+
}
|
|
1927
|
+
function stigToBaseline(s, resultsChecksum) {
|
|
1928
|
+
const baseline = createMinimalBaseline("STIG Checklist Scan", s.vulns.map(vulnToRequirement), { resultsChecksum });
|
|
1929
|
+
if (s.title) baseline.title = s.title;
|
|
1930
|
+
if (s.version) baseline.version = s.version;
|
|
1931
|
+
const ext = baselineExtensions(s);
|
|
1932
|
+
if (Object.keys(ext).length > 0) baseline.extensions = ext;
|
|
1933
|
+
return baseline;
|
|
1934
|
+
}
|
|
1935
|
+
function vulnToRequirement(v) {
|
|
1936
|
+
const severity = (v.severity ?? "").toLowerCase();
|
|
1937
|
+
const impact = severity ? severityToImpact(severity) : .5;
|
|
1938
|
+
const descriptions = [{
|
|
1939
|
+
label: "default",
|
|
1940
|
+
data: stripHTML(v.vulnDiscuss ?? "")
|
|
1941
|
+
}];
|
|
1942
|
+
if (v.checkContent) descriptions.push({
|
|
1943
|
+
label: "check",
|
|
1944
|
+
data: stripHTML(v.checkContent)
|
|
1945
|
+
});
|
|
1946
|
+
if (v.fixText) descriptions.push({
|
|
1947
|
+
label: "fix",
|
|
1948
|
+
data: stripHTML(v.fixText)
|
|
1949
|
+
});
|
|
1950
|
+
const message = [v.findingDetails, v.comments].map((s) => (s ?? "").trim()).filter(Boolean).join("\n\n");
|
|
1951
|
+
const result = createResult(statusToHdf(v.status), message, { codeDesc: `STIG rule ${v.ruleVer ?? ""}` });
|
|
1952
|
+
const tags = {};
|
|
1953
|
+
let nistTags = [];
|
|
1954
|
+
if (v.ccis.length > 0) {
|
|
1955
|
+
tags["cci"] = v.ccis;
|
|
1956
|
+
nistTags = [...new Set(v.ccis.flatMap((c) => getCCINistMappings(c) ?? []))].sort();
|
|
1957
|
+
tags["nist"] = nistTags;
|
|
1958
|
+
} else tags["nist"] = [];
|
|
1959
|
+
setIf(tags, "rid", v.ruleID);
|
|
1960
|
+
setIf(tags, "stig_id", v.ruleVer);
|
|
1961
|
+
setIf(tags, "gtitle", v.groupTitle);
|
|
1962
|
+
setIf(tags, "group_id", v.groupID);
|
|
1963
|
+
setIf(tags, "weight", v.weight);
|
|
1964
|
+
setIf(tags, "severity", severity);
|
|
1965
|
+
if (v.legacyIDs && v.legacyIDs.length) tags["legacy_ids"] = v.legacyIDs;
|
|
1966
|
+
if (v.extra && Object.keys(v.extra).length > 0) tags["cklMetadata"] = { ...v.extra };
|
|
1967
|
+
const req = createRequirement(v.vulnNum, v.ruleTitle ?? v.vulnNum, descriptions, impact, [result], { tags });
|
|
1968
|
+
if (severity) req.severity = severity;
|
|
1969
|
+
const controlType = deriveControlTypeFromTags(nistTags);
|
|
1970
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
1971
|
+
return req;
|
|
1972
|
+
}
|
|
1973
|
+
function assetToComponent(a) {
|
|
1974
|
+
if (!a.hostName && !a.hostIP && !a.hostFQDN) return void 0;
|
|
1975
|
+
const c = {
|
|
1976
|
+
name: a.hostName || a.hostFQDN || a.hostIP || "",
|
|
1977
|
+
type: TargetType.Host
|
|
1978
|
+
};
|
|
1979
|
+
if (a.hostIP) c.ipAddress = a.hostIP;
|
|
1980
|
+
if (a.hostFQDN) c.fqdn = a.hostFQDN;
|
|
1981
|
+
if (a.hostMAC) c.macAddress = a.hostMAC;
|
|
1982
|
+
return c;
|
|
1983
|
+
}
|
|
1984
|
+
function rootExtensions(cl) {
|
|
1985
|
+
const ext = { checklistFormat: cl.format || "ckl" };
|
|
1986
|
+
if (cl.cklbVersion) ext["cklbVersion"] = cl.cklbVersion;
|
|
1987
|
+
const ax = {};
|
|
1988
|
+
setIf(ax, "role", cl.asset.role);
|
|
1989
|
+
setIf(ax, "assetType", cl.asset.assetType);
|
|
1990
|
+
setIf(ax, "marking", cl.asset.marking);
|
|
1991
|
+
setIf(ax, "targetKey", cl.asset.targetKey);
|
|
1992
|
+
setIf(ax, "techArea", cl.asset.techArea);
|
|
1993
|
+
setIf(ax, "targetComment", cl.asset.targetComment);
|
|
1994
|
+
setIf(ax, "webDbSite", cl.asset.webDBSite);
|
|
1995
|
+
setIf(ax, "webDbInstance", cl.asset.webDBInstance);
|
|
1996
|
+
setIf(ax, "classification", cl.asset.classification);
|
|
1997
|
+
if (cl.asset.webOrDatabase) ax["webOrDatabase"] = true;
|
|
1998
|
+
if (Object.keys(ax).length > 0) ext["assetExtras"] = ax;
|
|
1999
|
+
return ext;
|
|
2000
|
+
}
|
|
2001
|
+
function baselineExtensions(s) {
|
|
2002
|
+
const ext = {};
|
|
2003
|
+
setIf(ext, "stigid", s.stigID);
|
|
2004
|
+
setIf(ext, "uuid", s.uuid);
|
|
2005
|
+
setIf(ext, "releaseInfo", s.releaseInfo);
|
|
2006
|
+
setIf(ext, "displayName", s.displayName);
|
|
2007
|
+
setIf(ext, "referenceIdentifier", s.referenceIdentifier);
|
|
2008
|
+
setIf(ext, "classification", s.classification);
|
|
2009
|
+
return ext;
|
|
2010
|
+
}
|
|
2011
|
+
function setIf(m, key, val) {
|
|
2012
|
+
if (val) m[key] = val;
|
|
2013
|
+
}
|
|
2014
|
+
//#endregion
|
|
2015
|
+
//#region shared/typescript/checklist/from-hdf.ts
|
|
2016
|
+
/**
|
|
2017
|
+
* Map HDF Results back to the format-neutral Checklist model. When the HDF
|
|
2018
|
+
* carries checklist passthrough (extensions/tags from checklistToHdf), the
|
|
2019
|
+
* original fields are reproduced losslessly; otherwise required checklist
|
|
2020
|
+
* fields are synthesized best-effort so any HDF yields a valid checklist.
|
|
2021
|
+
*/
|
|
2022
|
+
function hdfToChecklist(input) {
|
|
2023
|
+
const hdf = parseJSON(input);
|
|
2024
|
+
if (!hdf || !Array.isArray(hdf.baselines) || hdf.baselines.length === 0) throw new Error("hdf to checklist: HDF has no baselines");
|
|
2025
|
+
const ext = hdf.extensions ?? {};
|
|
2026
|
+
const format = strVal(ext, "checklistFormat") || "ckl";
|
|
2027
|
+
const cklbVersion = strVal(ext, "cklbVersion");
|
|
2028
|
+
const asset = buildAsset(hdf, ext);
|
|
2029
|
+
const stigs = hdf.baselines.map(baselineToStig);
|
|
2030
|
+
return {
|
|
2031
|
+
format,
|
|
2032
|
+
cklbVersion: cklbVersion || void 0,
|
|
2033
|
+
asset,
|
|
2034
|
+
stigs
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
function buildAsset(hdf, ext) {
|
|
2038
|
+
const asset = {};
|
|
2039
|
+
const comp = hdf.components?.[0];
|
|
2040
|
+
if (comp) {
|
|
2041
|
+
asset.hostName = comp.name;
|
|
2042
|
+
asset.hostIP = comp.ipAddress;
|
|
2043
|
+
asset.hostFQDN = comp.fqdn;
|
|
2044
|
+
asset.hostMAC = comp.macAddress;
|
|
2045
|
+
}
|
|
2046
|
+
const ax = ext["assetExtras"];
|
|
2047
|
+
if (ax && typeof ax === "object") {
|
|
2048
|
+
const a = ax;
|
|
2049
|
+
asset.role = strVal(a, "role");
|
|
2050
|
+
asset.assetType = strVal(a, "assetType");
|
|
2051
|
+
asset.marking = strVal(a, "marking");
|
|
2052
|
+
asset.targetKey = strVal(a, "targetKey");
|
|
2053
|
+
asset.techArea = strVal(a, "techArea");
|
|
2054
|
+
asset.targetComment = strVal(a, "targetComment");
|
|
2055
|
+
asset.webDBSite = strVal(a, "webDbSite");
|
|
2056
|
+
asset.webDBInstance = strVal(a, "webDbInstance");
|
|
2057
|
+
asset.classification = strVal(a, "classification");
|
|
2058
|
+
asset.webOrDatabase = a["webOrDatabase"] === true;
|
|
2059
|
+
}
|
|
2060
|
+
return asset;
|
|
2061
|
+
}
|
|
2062
|
+
function baselineToStig(bl) {
|
|
2063
|
+
const ext = bl.extensions ?? {};
|
|
2064
|
+
return {
|
|
2065
|
+
stigID: strVal(ext, "stigid") || bl.title || "",
|
|
2066
|
+
title: bl.title,
|
|
2067
|
+
version: bl.version,
|
|
2068
|
+
uuid: strVal(ext, "uuid"),
|
|
2069
|
+
releaseInfo: strVal(ext, "releaseInfo"),
|
|
2070
|
+
displayName: strVal(ext, "displayName"),
|
|
2071
|
+
referenceIdentifier: strVal(ext, "referenceIdentifier"),
|
|
2072
|
+
classification: strVal(ext, "classification"),
|
|
2073
|
+
vulns: bl.requirements.map(requirementToVuln)
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
function requirementToVuln(req) {
|
|
2077
|
+
const tags = req.tags ?? {};
|
|
2078
|
+
const descs = req.descriptions ?? [];
|
|
2079
|
+
return {
|
|
2080
|
+
vulnNum: req.id,
|
|
2081
|
+
ruleID: strVal(tags, "rid"),
|
|
2082
|
+
ruleVer: strVal(tags, "stig_id"),
|
|
2083
|
+
groupID: strVal(tags, "group_id") || req.id,
|
|
2084
|
+
groupTitle: strVal(tags, "gtitle"),
|
|
2085
|
+
ruleTitle: req.title,
|
|
2086
|
+
weight: strVal(tags, "weight"),
|
|
2087
|
+
severity: resolveSeverity(req, tags),
|
|
2088
|
+
vulnDiscuss: descData(descs, "default"),
|
|
2089
|
+
checkContent: descData(descs, "check"),
|
|
2090
|
+
fixText: descData(descs, "fix"),
|
|
2091
|
+
ccis: resolveCcis(tags),
|
|
2092
|
+
legacyIDs: strSlice(tags, "legacy_ids"),
|
|
2093
|
+
status: statusFromHdf(req.results?.[0]?.status),
|
|
2094
|
+
findingDetails: req.results?.[0]?.message,
|
|
2095
|
+
extra: extractCklMetadata(tags)
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
function resolveSeverity(req, tags) {
|
|
2099
|
+
const tagSev = strVal(tags, "severity");
|
|
2100
|
+
if (tagSev) return tagSev;
|
|
2101
|
+
if (req.severity) return String(req.severity).toLowerCase();
|
|
2102
|
+
const i = req.impact ?? 0;
|
|
2103
|
+
if (i >= .7) return "high";
|
|
2104
|
+
if (i >= .4) return "medium";
|
|
2105
|
+
if (i > 0) return "low";
|
|
2106
|
+
return "";
|
|
2107
|
+
}
|
|
2108
|
+
function resolveCcis(tags) {
|
|
2109
|
+
const direct = strSlice(tags, "cci");
|
|
2110
|
+
if (direct.length > 0) return direct;
|
|
2111
|
+
const nist = strSlice(tags, "nist");
|
|
2112
|
+
if (nist.length > 0) {
|
|
2113
|
+
const ccis = nistToCci(nist);
|
|
2114
|
+
if (ccis.length > 0) return ccis;
|
|
2115
|
+
}
|
|
2116
|
+
return [];
|
|
2117
|
+
}
|
|
2118
|
+
function extractCklMetadata(tags) {
|
|
2119
|
+
const meta = tags["cklMetadata"];
|
|
2120
|
+
if (!meta || typeof meta !== "object") return void 0;
|
|
2121
|
+
const out = {};
|
|
2122
|
+
for (const [k, v] of Object.entries(meta)) if (typeof v === "string") out[k] = v;
|
|
2123
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
2124
|
+
}
|
|
2125
|
+
function descData(descs, label) {
|
|
2126
|
+
return descs.find((d) => d.label === label)?.data ?? "";
|
|
2127
|
+
}
|
|
2128
|
+
function strVal(m, key) {
|
|
2129
|
+
const v = m[key];
|
|
2130
|
+
return typeof v === "string" ? v : "";
|
|
2131
|
+
}
|
|
2132
|
+
function strSlice(m, key) {
|
|
2133
|
+
const v = m[key];
|
|
2134
|
+
if (Array.isArray(v)) return v.filter((e) => typeof e === "string");
|
|
2135
|
+
return [];
|
|
2136
|
+
}
|
|
2137
|
+
//#endregion
|
|
2138
|
+
//#region converters/ckl-to-hdf/typescript/converter.ts
|
|
2139
|
+
/**
|
|
2140
|
+
* Convert a DISA STIG Viewer .ckl document to HDF Results JSON.
|
|
2141
|
+
*
|
|
2142
|
+
* Parsing and the HDF mapping live in the shared checklist module. v3.2
|
|
2143
|
+
* classification: controlType is derived per-VULN from CCI->NIST;
|
|
2144
|
+
* verificationMethod and applicability are omitted (the checklist format
|
|
2145
|
+
* cannot substantiate them — see the Go package doc and build-converter
|
|
2146
|
+
* skill Step 4d).
|
|
2147
|
+
*/
|
|
2148
|
+
async function convertCklToHdf(input) {
|
|
2149
|
+
validateInputSize(input, "ckl-to-hdf");
|
|
2150
|
+
const resultsChecksum = await inputChecksum(input);
|
|
2151
|
+
const checklist = parseCkl(input);
|
|
2152
|
+
return JSON.stringify(checklistToHdf(checklist, resultsChecksum, "ckl-to-hdf"), null, 2);
|
|
2153
|
+
}
|
|
2154
|
+
//#endregion
|
|
2155
|
+
//#region converters/cklb-to-hdf/typescript/converter.ts
|
|
2156
|
+
/**
|
|
2157
|
+
* Convert a DISA STIG Viewer 3.x .cklb document to HDF Results JSON.
|
|
2158
|
+
*
|
|
2159
|
+
* Parsing and the HDF mapping live in the shared checklist module. v3.2
|
|
2160
|
+
* classification: controlType is derived per-rule from CCI->NIST;
|
|
2161
|
+
* verificationMethod and applicability are omitted (the checklist format
|
|
2162
|
+
* cannot substantiate them — see the Go package doc and build-converter
|
|
2163
|
+
* skill Step 4d).
|
|
2164
|
+
*/
|
|
2165
|
+
async function convertCklbToHdf(input) {
|
|
2166
|
+
validateInputSize(input, "cklb-to-hdf");
|
|
2167
|
+
const resultsChecksum = await inputChecksum(input);
|
|
2168
|
+
const checklist = parseCklb(input);
|
|
2169
|
+
return JSON.stringify(checklistToHdf(checklist, resultsChecksum, "cklb-to-hdf"), null, 2);
|
|
2170
|
+
}
|
|
2171
|
+
//#endregion
|
|
2172
|
+
//#region converters/hdf-to-ckl/typescript/converter.ts
|
|
2173
|
+
/**
|
|
2174
|
+
* Convert HDF Results JSON into a DISA STIG Viewer checklist (.ckl XML).
|
|
2175
|
+
*
|
|
2176
|
+
* This is a thin wrapper over the shared checklist module, mirroring the
|
|
2177
|
+
* hdf-to-csv reverse converter. It produces a STIG Viewer 2.x .ckl from ANY
|
|
2178
|
+
* HDF: when the HDF carries checklist passthrough (extensions/tags written by
|
|
2179
|
+
* a prior ckl/cklb-to-hdf conversion) the original fields are reproduced
|
|
2180
|
+
* losslessly; otherwise the required checklist fields are synthesized
|
|
2181
|
+
* best-effort (id->Vuln_Num, nist->CCI reverse, status reverse) with safe
|
|
2182
|
+
* defaults. Mirrors heimdall2 PR #4841.
|
|
2183
|
+
*
|
|
2184
|
+
* @param input HDF Results JSON string
|
|
2185
|
+
* @returns CKL XML string (with the `<?xml ...?>` header)
|
|
2186
|
+
*/
|
|
2187
|
+
function convertHdfToCkl(input) {
|
|
2188
|
+
return serializeCkl(hdfToChecklist(input));
|
|
2189
|
+
}
|
|
2190
|
+
//#endregion
|
|
2191
|
+
//#region converters/hdf-to-cklb/typescript/converter.ts
|
|
2192
|
+
/**
|
|
2193
|
+
* Convert HDF Results JSON into a DISA STIG Viewer 3.x checklist (.cklb, JSON).
|
|
2194
|
+
*
|
|
2195
|
+
* The HDF->checklist mapping and CKLB serialization live in the shared
|
|
2196
|
+
* checklist module; this converter is a thin wrapper. Any HDF input produces a
|
|
2197
|
+
* valid CKLB: when the HDF carries checklist passthrough (it originated from a
|
|
2198
|
+
* CKL/CKLB via the reverse converters), the original fields are reproduced
|
|
2199
|
+
* losslessly; otherwise the required checklist fields are synthesized
|
|
2200
|
+
* best-effort from the HDF requirements, tags, and results.
|
|
2201
|
+
*
|
|
2202
|
+
* @param input HDF Results JSON string
|
|
2203
|
+
* @returns CKLB JSON string
|
|
2204
|
+
*/
|
|
2205
|
+
function convertHdfToCklb(input) {
|
|
2206
|
+
return serializeCklb(hdfToChecklist(input));
|
|
2207
|
+
}
|
|
2208
|
+
//#endregion
|
|
1213
2209
|
//#region converters/snyk-to-hdf/typescript/converter.ts
|
|
1214
2210
|
/**
|
|
1215
2211
|
* Formats the "from" array as a human-readable dependency path.
|
|
@@ -1221,7 +2217,28 @@ function formatDependencyPath(from) {
|
|
|
1221
2217
|
/**
|
|
1222
2218
|
* Builds a single EvaluatedRequirement from a group of vulnerabilities sharing an ID.
|
|
1223
2219
|
*/
|
|
1224
|
-
|
|
2220
|
+
/**
|
|
2221
|
+
* Map Snyk's `packageManager` value to an Affected_Package ecosystem.
|
|
2222
|
+
* Snyk reports values that don't always match PURL types one-to-one
|
|
2223
|
+
* (pip → pypi, rubygems → gem, yarn → npm). Unknown managers fall back
|
|
2224
|
+
* to `generic`.
|
|
2225
|
+
*/
|
|
2226
|
+
function ecosystemFromSnykPackageManager(pm) {
|
|
2227
|
+
if (!pm) return Ecosystem.Generic;
|
|
2228
|
+
const lower = pm.toLowerCase();
|
|
2229
|
+
if (lower === "pip" || lower === "pip3") return Ecosystem.Pypi;
|
|
2230
|
+
if (lower === "rubygems" || lower === "bundler") return Ecosystem.Gem;
|
|
2231
|
+
if (lower === "yarn" || lower === "npm") return Ecosystem.Npm;
|
|
2232
|
+
return ecosystemFromPurlType(lower);
|
|
2233
|
+
}
|
|
2234
|
+
/** Synthesize a `pkg:<type>/<name>@<version>` PURL when the ecosystem
|
|
2235
|
+
* maps cleanly. Returns undefined for `generic` so we don't emit a
|
|
2236
|
+
* fake `pkg:generic/...` PURL that downstream tools can't dereference. */
|
|
2237
|
+
function synthesizePurl(ecosystem, name, version) {
|
|
2238
|
+
if (ecosystem === Ecosystem.Generic) return void 0;
|
|
2239
|
+
return `pkg:${ecosystem}/${name}@${version}`;
|
|
2240
|
+
}
|
|
2241
|
+
function buildRequirement$16(vulnID, vulns, packageManager) {
|
|
1225
2242
|
const rep = vulns[0];
|
|
1226
2243
|
const cweIDs = rep.identifiers.CWE ?? [];
|
|
1227
2244
|
const nist = mapCWEToNIST(cweIDs, DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
|
|
@@ -1237,7 +2254,24 @@ function buildRequirement$15(vulnID, vulns) {
|
|
|
1237
2254
|
data: rep.description
|
|
1238
2255
|
}];
|
|
1239
2256
|
const results = vulns.map((vuln) => createResult(ResultStatus.Failed, void 0, { codeDesc: formatDependencyPath(vuln.from) }));
|
|
1240
|
-
|
|
2257
|
+
const req = createRequirement(vulnID, rep.title, descriptions, severityToImpact(rep.severity), results, { tags });
|
|
2258
|
+
const controlType = deriveControlTypeFromTags(nist);
|
|
2259
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
2260
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
2261
|
+
const name = rep.packageName ?? rep.moduleName;
|
|
2262
|
+
const version = rep.version;
|
|
2263
|
+
if (name && version) {
|
|
2264
|
+
const ecosystem = ecosystemFromSnykPackageManager(packageManager);
|
|
2265
|
+
const pkg = buildAffectedPackage$1({
|
|
2266
|
+
name,
|
|
2267
|
+
version,
|
|
2268
|
+
ecosystem,
|
|
2269
|
+
purl: synthesizePurl(ecosystem, name, version),
|
|
2270
|
+
fixedInVersion: rep.fixedIn?.[0]
|
|
2271
|
+
});
|
|
2272
|
+
if (pkg) req.affectedPackages = [pkg];
|
|
2273
|
+
}
|
|
2274
|
+
return req;
|
|
1241
2275
|
}
|
|
1242
2276
|
/**
|
|
1243
2277
|
* Converts a single Snyk project report to an HDF baseline.
|
|
@@ -1253,7 +2287,11 @@ function convertSingleProject(report, resultsChecksum) {
|
|
|
1253
2287
|
else groups.set(vuln.id, [vuln]);
|
|
1254
2288
|
}
|
|
1255
2289
|
const requirements = [];
|
|
1256
|
-
for (const [vulnID, vulns] of groups) requirements.push(buildRequirement$
|
|
2290
|
+
for (const [vulnID, vulns] of groups) requirements.push(buildRequirement$16(vulnID, vulns, report.packageManager));
|
|
2291
|
+
if (requirements.length === 0) {
|
|
2292
|
+
const target = report.projectName ?? report.path ?? "project";
|
|
2293
|
+
requirements.push(buildNoFindingsRequirement("snyk-no-findings", `Snyk scanned ${target} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
|
|
2294
|
+
}
|
|
1257
2295
|
return createMinimalBaseline("Snyk Scan", requirements, {
|
|
1258
2296
|
resultsChecksum,
|
|
1259
2297
|
title: `Snyk Project: ${report.projectName ?? ""} Snyk Path: ${report.path ?? ""}`,
|
|
@@ -1298,7 +2336,7 @@ async function convertSnykToHdf(input) {
|
|
|
1298
2336
|
baselines,
|
|
1299
2337
|
components: [{
|
|
1300
2338
|
name: targetName,
|
|
1301
|
-
type:
|
|
2339
|
+
type: TargetType.Application
|
|
1302
2340
|
}],
|
|
1303
2341
|
timestamp: /* @__PURE__ */ new Date()
|
|
1304
2342
|
});
|
|
@@ -1313,7 +2351,7 @@ const IMPACT_MAPPING$8 = new Map([
|
|
|
1313
2351
|
["negligible", 0],
|
|
1314
2352
|
["unknown", .5]
|
|
1315
2353
|
]);
|
|
1316
|
-
function getImpact$
|
|
2354
|
+
function getImpact$9(severity) {
|
|
1317
2355
|
if (!severity) return .5;
|
|
1318
2356
|
return IMPACT_MAPPING$8.get(severity.toLowerCase()) ?? .5;
|
|
1319
2357
|
}
|
|
@@ -1366,11 +2404,97 @@ function buildCodeDesc$8(match) {
|
|
|
1366
2404
|
}
|
|
1367
2405
|
return parts.join(" | ");
|
|
1368
2406
|
}
|
|
2407
|
+
function cvssVersionToSchema(v) {
|
|
2408
|
+
switch (v) {
|
|
2409
|
+
case "2.0": return Version.The20;
|
|
2410
|
+
case "3.0": return Version.The30;
|
|
2411
|
+
case "4.0": return Version.The40;
|
|
2412
|
+
default: return Version.The31;
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
function cvssBandSeverity(score) {
|
|
2416
|
+
switch (cvssScoreToSeverity(score)) {
|
|
2417
|
+
case "critical": return CVSSSeverity.Critical;
|
|
2418
|
+
case "high": return CVSSSeverity.High;
|
|
2419
|
+
case "medium": return CVSSSeverity.Medium;
|
|
2420
|
+
case "low": return CVSSSeverity.Low;
|
|
2421
|
+
default: return CVSSSeverity.None;
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
function buildCvssEntries$1(vuln) {
|
|
2425
|
+
if (!vuln.cvss || vuln.cvss.length === 0) return;
|
|
2426
|
+
const entries = [];
|
|
2427
|
+
for (const c of vuln.cvss) {
|
|
2428
|
+
const entry = { version: cvssVersionToSchema(c.version) };
|
|
2429
|
+
if (c.vector) entry.baseVector = c.vector;
|
|
2430
|
+
const score = c.metrics?.baseScore;
|
|
2431
|
+
if (typeof score === "number" && Number.isFinite(score)) {
|
|
2432
|
+
entry.baseScore = score;
|
|
2433
|
+
entry.baseSeverity = cvssBandSeverity(score);
|
|
2434
|
+
}
|
|
2435
|
+
if (vuln.id) entry.source = vuln.id;
|
|
2436
|
+
if (entry.baseVector === void 0 && entry.baseScore === void 0) continue;
|
|
2437
|
+
entries.push(entry);
|
|
2438
|
+
}
|
|
2439
|
+
return entries.length > 0 ? entries : void 0;
|
|
2440
|
+
}
|
|
2441
|
+
function mapGrypeTypeToEcosystem(grypeType) {
|
|
2442
|
+
switch ((grypeType ?? "").toLowerCase()) {
|
|
2443
|
+
case "rpm": return Ecosystem.RPM;
|
|
2444
|
+
case "deb": return Ecosystem.Deb;
|
|
2445
|
+
case "npm": return Ecosystem.Npm;
|
|
2446
|
+
case "python": return Ecosystem.Pypi;
|
|
2447
|
+
case "gem": return Ecosystem.Gem;
|
|
2448
|
+
case "go-module": return Ecosystem.Go;
|
|
2449
|
+
case "java-archive":
|
|
2450
|
+
case "jenkins-plugin": return Ecosystem.Maven;
|
|
2451
|
+
case "dotnet": return Ecosystem.Nuget;
|
|
2452
|
+
case "rust-crate": return Ecosystem.Cargo;
|
|
2453
|
+
default: return Ecosystem.Generic;
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
function buildAffectedPackages(match) {
|
|
2457
|
+
const artifact = match.artifact;
|
|
2458
|
+
const pkg = {
|
|
2459
|
+
name: artifact.name,
|
|
2460
|
+
version: artifact.version,
|
|
2461
|
+
ecosystem: mapGrypeTypeToEcosystem(artifact.type)
|
|
2462
|
+
};
|
|
2463
|
+
if (artifact.cpes && artifact.cpes.length > 0 && artifact.cpes[0]) pkg.cpe = artifact.cpes[0];
|
|
2464
|
+
if (artifact.purl) pkg.purl = artifact.purl;
|
|
2465
|
+
const fix = match.vulnerability.fix;
|
|
2466
|
+
if (fix && fix.state === "fixed" && fix.versions && fix.versions.length > 0 && fix.versions[0]) pkg.fixedInVersion = fix.versions[0];
|
|
2467
|
+
return [pkg];
|
|
2468
|
+
}
|
|
2469
|
+
const CWE_ID_PATTERN = /^CWE-[1-9]\d*$/;
|
|
2470
|
+
function extractCwe(raw) {
|
|
2471
|
+
if (!raw || raw.length === 0) return void 0;
|
|
2472
|
+
const out = raw.filter((c) => CWE_ID_PATTERN.test(c));
|
|
2473
|
+
return out.length === 0 ? void 0 : out;
|
|
2474
|
+
}
|
|
2475
|
+
function buildEpss$1(entries) {
|
|
2476
|
+
if (!entries || entries.length === 0) return void 0;
|
|
2477
|
+
const e = entries[0];
|
|
2478
|
+
if (!e.date) return void 0;
|
|
2479
|
+
return {
|
|
2480
|
+
score: e.epss ?? 0,
|
|
2481
|
+
percentile: e.percentile ?? 0,
|
|
2482
|
+
date: e.date
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
function buildKev(k) {
|
|
2486
|
+
if (!k) return void 0;
|
|
2487
|
+
const out = { inKev: Boolean(k.inKev) };
|
|
2488
|
+
if (k.dateAdded) out.dateAdded = k.dateAdded;
|
|
2489
|
+
if (k.dueDate) out.dueDate = k.dueDate;
|
|
2490
|
+
if (k.notes) out.notes = k.notes;
|
|
2491
|
+
return out;
|
|
2492
|
+
}
|
|
1369
2493
|
function convertMatchToRequirement(match, isIgnored) {
|
|
1370
2494
|
const vuln = match.vulnerability;
|
|
1371
2495
|
const cveId = vuln.id;
|
|
1372
2496
|
const severity = vuln.severity;
|
|
1373
|
-
const impact = getImpact$
|
|
2497
|
+
const impact = getImpact$9(severity);
|
|
1374
2498
|
const description = getDescription(vuln, match.relatedVulnerabilities);
|
|
1375
2499
|
const fixInfo = getFixInfo(vuln.fix);
|
|
1376
2500
|
const cvssInfo = getCVSSInfo(vuln, match.relatedVulnerabilities);
|
|
@@ -1392,7 +2516,7 @@ function convertMatchToRequirement(match, isIgnored) {
|
|
|
1392
2516
|
startTime: /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
|
|
1393
2517
|
};
|
|
1394
2518
|
const tags = buildNistCciTags(DEFAULT_STATIC_ANALYSIS_NIST_TAGS, nistToCci(DEFAULT_STATIC_ANALYSIS_NIST_TAGS));
|
|
1395
|
-
|
|
2519
|
+
const requirement = {
|
|
1396
2520
|
id: isIgnored ? `Grype-Ignored-Match/${cveId}` : `Grype/${cveId}`,
|
|
1397
2521
|
impact,
|
|
1398
2522
|
results: [result],
|
|
@@ -1411,8 +2535,21 @@ function convertMatchToRequirement(match, isIgnored) {
|
|
|
1411
2535
|
data: cvssInfo
|
|
1412
2536
|
}
|
|
1413
2537
|
],
|
|
1414
|
-
refs: refs.length > 0 ? refs.map((url) => ({ url })) : void 0
|
|
2538
|
+
refs: refs.length > 0 ? refs.map((url) => ({ url })) : void 0,
|
|
2539
|
+
verificationMethod: VerificationMethodEnum.Automated
|
|
1415
2540
|
};
|
|
2541
|
+
const controlType = deriveControlTypeFromTags(DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
|
|
2542
|
+
if (controlType !== void 0) requirement.controlType = controlType;
|
|
2543
|
+
const cvss = buildCvssEntries$1(vuln);
|
|
2544
|
+
if (cvss) requirement.cvss = cvss;
|
|
2545
|
+
requirement.affectedPackages = buildAffectedPackages(match);
|
|
2546
|
+
const cwe = extractCwe(vuln.cwe);
|
|
2547
|
+
if (cwe) requirement.cwe = cwe;
|
|
2548
|
+
const epss = buildEpss$1(vuln.epss);
|
|
2549
|
+
if (epss) requirement.epss = epss;
|
|
2550
|
+
const kev = buildKev(vuln.kev);
|
|
2551
|
+
if (kev) requirement.kev = kev;
|
|
2552
|
+
return requirement;
|
|
1416
2553
|
}
|
|
1417
2554
|
async function convertGrypeToHdf(input) {
|
|
1418
2555
|
validateInputSize(input, "grype");
|
|
@@ -1432,6 +2569,7 @@ async function convertGrypeToHdf(input) {
|
|
|
1432
2569
|
for (const match of limitedIgnored) requirements.push(convertMatchToRequirement(match, true));
|
|
1433
2570
|
}
|
|
1434
2571
|
const targetName = grypeData.source?.target?.userInput || "Grype Scan";
|
|
2572
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("grype-no-findings", `Grype scanned ${targetName} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
|
|
1435
2573
|
const baseline = createMinimalBaseline(targetName, requirements, { resultsChecksum });
|
|
1436
2574
|
return buildHdfResults({
|
|
1437
2575
|
generatorName: grypeData.descriptor?.name || "grype",
|
|
@@ -1440,7 +2578,7 @@ async function convertGrypeToHdf(input) {
|
|
|
1440
2578
|
toolVersion: grypeData.descriptor?.version,
|
|
1441
2579
|
baselines: [baseline],
|
|
1442
2580
|
components: [{
|
|
1443
|
-
type:
|
|
2581
|
+
type: TargetType.Artifact,
|
|
1444
2582
|
name: targetName
|
|
1445
2583
|
}],
|
|
1446
2584
|
timestamp: grypeData.descriptor?.timestamp ? new Date(grypeData.descriptor.timestamp) : /* @__PURE__ */ new Date()
|
|
@@ -1448,6 +2586,8 @@ async function convertGrypeToHdf(input) {
|
|
|
1448
2586
|
}
|
|
1449
2587
|
//#endregion
|
|
1450
2588
|
//#region converters/nessus-to-hdf/typescript/converter.ts
|
|
2589
|
+
const CVE_SOURCE_RE = /^CVE-\d{4}-\d{4,}$/;
|
|
2590
|
+
const CWE_PATTERN = /CWE[- ]?(\d+)/gi;
|
|
1451
2591
|
const converterVersion = "1.0.0";
|
|
1452
2592
|
const IMPACT_MAPPING$7 = {
|
|
1453
2593
|
"4": .9,
|
|
@@ -1475,7 +2615,9 @@ async function convertNessusToHdf(nessusXml) {
|
|
|
1475
2615
|
"preference",
|
|
1476
2616
|
"tag",
|
|
1477
2617
|
"ReportItem",
|
|
1478
|
-
"ReportHost"
|
|
2618
|
+
"ReportHost",
|
|
2619
|
+
"cwe",
|
|
2620
|
+
"cve"
|
|
1479
2621
|
]);
|
|
1480
2622
|
const policyName = parsed.NessusClientData_v2.Policy.policyName;
|
|
1481
2623
|
const version = extractVersion(parsed);
|
|
@@ -1497,7 +2639,7 @@ async function convertNessusToHdf(nessusXml) {
|
|
|
1497
2639
|
components,
|
|
1498
2640
|
statistics: { duration },
|
|
1499
2641
|
generator: {
|
|
1500
|
-
name: "hdf
|
|
2642
|
+
name: "nessus-to-hdf",
|
|
1501
2643
|
version: converterVersion
|
|
1502
2644
|
},
|
|
1503
2645
|
tool: { name: "Nessus" },
|
|
@@ -1544,6 +2686,12 @@ function convertReportHostToBaseline(host, policyName, version, resultsChecksum)
|
|
|
1544
2686
|
if (truncatedItems) console.warn(`WARNING: Input truncated at ${limitedItems.length} ReportItem items (original: ${items.length})`);
|
|
1545
2687
|
requirements = limitedItems.map((item) => convertReportItemToRequirement(item, host));
|
|
1546
2688
|
} else requirements = [];
|
|
2689
|
+
if (requirements.length === 0) {
|
|
2690
|
+
const target = host.name || getHostPropertyValue(host, "host-ip") || "host";
|
|
2691
|
+
const startTimeStr = getHostPropertyValue(host, "HOST_START");
|
|
2692
|
+
const startTime = startTimeStr ? new Date(startTimeStr) : /* @__PURE__ */ new Date();
|
|
2693
|
+
requirements = [buildNoFindingsRequirement("nessus-no-findings", `Nessus scanned ${target} and reported zero findings.`, startTime)];
|
|
2694
|
+
}
|
|
1547
2695
|
return createMinimalBaseline(`Nessus ${policyName}`, requirements, {
|
|
1548
2696
|
title: `Nessus ${policyName}`,
|
|
1549
2697
|
version,
|
|
@@ -1554,17 +2702,164 @@ function convertReportHostToBaseline(host, policyName, version, resultsChecksum)
|
|
|
1554
2702
|
}
|
|
1555
2703
|
function convertReportItemToRequirement(item, host) {
|
|
1556
2704
|
const isCompliance = !!item["compliance-reference"];
|
|
2705
|
+
const id = isCompliance ? parseComplianceRef(item["compliance-reference"], "Vuln-ID")[0] || item["pluginID"] : item["pluginID"];
|
|
2706
|
+
const title = isCompliance ? item["compliance-check-name"] || item["pluginName"] : item["pluginName"];
|
|
2707
|
+
const descriptions = buildDescriptions(item, isCompliance);
|
|
2708
|
+
const impact = calculateImpact(item, isCompliance);
|
|
2709
|
+
const tags = buildTags$2(item, isCompliance);
|
|
2710
|
+
const refs = buildRefs(item);
|
|
2711
|
+
const results = [buildResult$1(item, host, isCompliance)];
|
|
2712
|
+
const code = JSON.stringify(item, null, 2);
|
|
2713
|
+
const nistTags = tags["nist"];
|
|
2714
|
+
const controlType = deriveControlTypeFromTags(nistTags ?? []);
|
|
2715
|
+
const verificationMethod = deriveVerificationMethod(code);
|
|
2716
|
+
const req = {
|
|
2717
|
+
id,
|
|
2718
|
+
title,
|
|
2719
|
+
descriptions,
|
|
2720
|
+
impact,
|
|
2721
|
+
tags,
|
|
2722
|
+
refs,
|
|
2723
|
+
results,
|
|
2724
|
+
code
|
|
2725
|
+
};
|
|
2726
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
2727
|
+
if (verificationMethod !== void 0) req.verificationMethod = verificationMethod;
|
|
2728
|
+
if (!isCompliance) {
|
|
2729
|
+
const cvssEntries = buildCvssEntries(item);
|
|
2730
|
+
if (cvssEntries.length > 0) req.cvss = cvssEntries;
|
|
2731
|
+
const cweIDs = buildCweIDs(item);
|
|
2732
|
+
if (cweIDs.length > 0) req.cwe = cweIDs;
|
|
2733
|
+
const epss = buildEpss(item, host);
|
|
2734
|
+
if (epss !== void 0) req.epss = epss;
|
|
2735
|
+
}
|
|
2736
|
+
return req;
|
|
2737
|
+
}
|
|
2738
|
+
/**
|
|
2739
|
+
* Build a structured Cvss entry for a CVE finding. Returns an array because
|
|
2740
|
+
* the schema models cvss as a multi-entry array (Nessus emits one entry per
|
|
2741
|
+
* item; multi-vendor convergence may yield more).
|
|
2742
|
+
*/
|
|
2743
|
+
function buildCvssEntries(item) {
|
|
2744
|
+
const source = (item.cvss_score_source || "").trim();
|
|
2745
|
+
if (!source || !CVE_SOURCE_RE.test(source)) return [];
|
|
2746
|
+
const hasV3 = !!(item.cvss3_vector || item.cvss3_base_score);
|
|
2747
|
+
const hasV2 = !!(item.cvss_vector || item.cvss_base_score);
|
|
2748
|
+
if (!hasV3 && !hasV2) return [];
|
|
2749
|
+
let version;
|
|
2750
|
+
let baseVector;
|
|
2751
|
+
let baseScore;
|
|
2752
|
+
let threatVector;
|
|
2753
|
+
let threatScore;
|
|
2754
|
+
if (hasV3) {
|
|
2755
|
+
version = detectV3Version(item.cvss3_vector ?? "");
|
|
2756
|
+
baseVector = item.cvss3_vector || void 0;
|
|
2757
|
+
baseScore = parseFloatOrUndef(item.cvss3_base_score);
|
|
2758
|
+
threatVector = stripVersionPrefix(item.cvss3_temporal_vector);
|
|
2759
|
+
threatScore = item.cvss3_temporal_score ? parseFloatSafe(item.cvss3_temporal_score) : void 0;
|
|
2760
|
+
} else {
|
|
2761
|
+
version = Version.The20;
|
|
2762
|
+
baseVector = stripV2Prefix(item.cvss_vector ?? "") || void 0;
|
|
2763
|
+
baseScore = parseFloatOrUndef(item.cvss_base_score);
|
|
2764
|
+
threatVector = stripV2Prefix(item.cvss_temporal_vector ?? "") || void 0;
|
|
2765
|
+
threatScore = item.cvss_temporal_score ? parseFloatSafe(item.cvss_temporal_score) : void 0;
|
|
2766
|
+
}
|
|
2767
|
+
const entry = {
|
|
2768
|
+
version,
|
|
2769
|
+
source
|
|
2770
|
+
};
|
|
2771
|
+
if (baseVector) entry.baseVector = baseVector;
|
|
2772
|
+
if (baseScore !== void 0) {
|
|
2773
|
+
entry.baseScore = baseScore;
|
|
2774
|
+
const baseSeverity = mapCvssSeverity(baseScore);
|
|
2775
|
+
if (baseSeverity !== void 0) entry.baseSeverity = baseSeverity;
|
|
2776
|
+
}
|
|
2777
|
+
if (threatVector !== void 0 && threatVector !== "") entry.threatVector = threatVector;
|
|
2778
|
+
if (threatScore !== void 0) {
|
|
2779
|
+
entry.threatScore = threatScore;
|
|
2780
|
+
entry.computedScore = threatScore;
|
|
2781
|
+
const computedSeverity = mapCvssSeverity(threatScore);
|
|
2782
|
+
if (computedSeverity !== void 0) entry.computedSeverity = computedSeverity;
|
|
2783
|
+
}
|
|
2784
|
+
return [entry];
|
|
2785
|
+
}
|
|
2786
|
+
function detectV3Version(vector) {
|
|
2787
|
+
if (vector.startsWith("CVSS:3.1/")) return Version.The31;
|
|
2788
|
+
if (vector.startsWith("CVSS:3.0/")) return Version.The30;
|
|
2789
|
+
return Version.The30;
|
|
2790
|
+
}
|
|
2791
|
+
function stripVersionPrefix(vector) {
|
|
2792
|
+
if (!vector) return void 0;
|
|
2793
|
+
for (const prefix of [
|
|
2794
|
+
"CVSS:3.0/",
|
|
2795
|
+
"CVSS:3.1/",
|
|
2796
|
+
"CVSS:4.0/"
|
|
2797
|
+
]) if (vector.startsWith(prefix)) return vector.slice(prefix.length);
|
|
2798
|
+
return vector;
|
|
2799
|
+
}
|
|
2800
|
+
function stripV2Prefix(vector) {
|
|
2801
|
+
return vector.startsWith("CVSS2#") ? vector.slice(6) : vector;
|
|
2802
|
+
}
|
|
2803
|
+
function parseFloatSafe(s) {
|
|
2804
|
+
if (!s) return 0;
|
|
2805
|
+
const f = Number.parseFloat(s);
|
|
2806
|
+
return Number.isFinite(f) ? f : 0;
|
|
2807
|
+
}
|
|
2808
|
+
function parseFloatOrUndef(s) {
|
|
2809
|
+
if (s === void 0 || s === "") return void 0;
|
|
2810
|
+
const f = Number.parseFloat(s);
|
|
2811
|
+
return Number.isFinite(f) ? f : void 0;
|
|
2812
|
+
}
|
|
2813
|
+
function mapCvssSeverity(score) {
|
|
2814
|
+
switch (cvssScoreToSeverity(score)) {
|
|
2815
|
+
case "critical": return CVSSSeverity.Critical;
|
|
2816
|
+
case "high": return CVSSSeverity.High;
|
|
2817
|
+
case "medium": return CVSSSeverity.Medium;
|
|
2818
|
+
case "low": return CVSSSeverity.Low;
|
|
2819
|
+
case "none": return CVSSSeverity.None;
|
|
2820
|
+
default: return;
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
/**
|
|
2824
|
+
* Extract CWE IDs from a ReportItem's <cwe> elements. Nessus emits bare
|
|
2825
|
+
* numeric IDs (e.g. <cwe>200</cwe>); occasionally pipe-separated or prefixed
|
|
2826
|
+
* forms appear. Output is "CWE-N" form per schema convention.
|
|
2827
|
+
*/
|
|
2828
|
+
function buildCweIDs(item) {
|
|
2829
|
+
if (!item.cwe) return [];
|
|
2830
|
+
const raws = Array.isArray(item.cwe) ? item.cwe : [item.cwe];
|
|
2831
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2832
|
+
for (const raw of raws) {
|
|
2833
|
+
const text = String(raw);
|
|
2834
|
+
for (const m of text.matchAll(CWE_PATTERN)) if (m[1]) seen.add(m[1]);
|
|
2835
|
+
for (const tok of text.split(/[^0-9]+/)) if (tok !== "") seen.add(tok);
|
|
2836
|
+
}
|
|
2837
|
+
if (seen.size === 0) return [];
|
|
2838
|
+
return [...seen].sort().map((id) => `CWE-${id}`);
|
|
2839
|
+
}
|
|
2840
|
+
/**
|
|
2841
|
+
* Build a structured Epss entry when the ReportItem includes EPSS data.
|
|
2842
|
+
* The date is derived from the host's HOST_START in YYYY-MM-DD form.
|
|
2843
|
+
*/
|
|
2844
|
+
function buildEpss(item, host) {
|
|
2845
|
+
const hasScore = item.epss_score !== void 0 && item.epss_score !== "";
|
|
2846
|
+
const hasPct = item.epss_percentile !== void 0 && item.epss_percentile !== "";
|
|
2847
|
+
if (!hasScore && !hasPct) return void 0;
|
|
2848
|
+
const date = epssDate(host);
|
|
2849
|
+
if (date === void 0) return void 0;
|
|
1557
2850
|
return {
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
impact: calculateImpact(item, isCompliance),
|
|
1562
|
-
tags: buildTags$2(item, isCompliance),
|
|
1563
|
-
refs: buildRefs(item),
|
|
1564
|
-
results: [buildResult$1(item, host, isCompliance)],
|
|
1565
|
-
code: JSON.stringify(item, null, 2)
|
|
2851
|
+
date,
|
|
2852
|
+
score: hasScore ? parseFloatSafe(item.epss_score) : 0,
|
|
2853
|
+
percentile: hasPct ? parseFloatSafe(item.epss_percentile) : 0
|
|
1566
2854
|
};
|
|
1567
2855
|
}
|
|
2856
|
+
function epssDate(host) {
|
|
2857
|
+
const hs = getHostPropertyValue(host, "HOST_START");
|
|
2858
|
+
if (hs) {
|
|
2859
|
+
const d = new Date(hs);
|
|
2860
|
+
if (!Number.isNaN(d.getTime())) return d.toISOString().slice(0, 10);
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
1568
2863
|
function buildDescriptions(item, isCompliance) {
|
|
1569
2864
|
const descriptions = [];
|
|
1570
2865
|
if (isCompliance && item["compliance-info"]) descriptions.push({
|
|
@@ -1625,7 +2920,7 @@ function buildTags$2(item, isCompliance) {
|
|
|
1625
2920
|
}
|
|
1626
2921
|
function buildRefs(item) {
|
|
1627
2922
|
const refs = [];
|
|
1628
|
-
if (item.see_also) refs.push({ url
|
|
2923
|
+
if (item.see_also) for (const url of item.see_also.split(/\s+/).filter(Boolean)) refs.push({ url });
|
|
1629
2924
|
return refs.length > 0 ? refs : void 0;
|
|
1630
2925
|
}
|
|
1631
2926
|
function buildResult$1(item, host, isCompliance) {
|
|
@@ -1667,7 +2962,7 @@ function convertReportHostToTarget(host) {
|
|
|
1667
2962
|
});
|
|
1668
2963
|
const target = {
|
|
1669
2964
|
name: hostName,
|
|
1670
|
-
type:
|
|
2965
|
+
type: TargetType.Host
|
|
1671
2966
|
};
|
|
1672
2967
|
if (isFQDN(hostName)) target.fqdn = hostName;
|
|
1673
2968
|
const hostIp = hostProps["host-ip"];
|
|
@@ -1735,18 +3030,40 @@ async function convertSonarqubeToHdf(input) {
|
|
|
1735
3030
|
const baseline = convertProjectToBaseline(projectKey, issues, componentMap, ruleMap, resultsChecksum);
|
|
1736
3031
|
baselines.push(baseline);
|
|
1737
3032
|
}
|
|
3033
|
+
let components = Array.from(issuesByProject.keys()).map((projectKey) => ({
|
|
3034
|
+
type: TargetType.Application,
|
|
3035
|
+
name: projectKey
|
|
3036
|
+
}));
|
|
3037
|
+
if (baselines.length === 0) {
|
|
3038
|
+
const targetName = deriveEmptyScanTarget(sonarData.components);
|
|
3039
|
+
baselines.push({
|
|
3040
|
+
name: targetName,
|
|
3041
|
+
title: `SonarQube Analysis for ${targetName}`,
|
|
3042
|
+
requirements: [buildNoFindingsRequirement("sonarqube-no-findings", `SonarQube scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date())],
|
|
3043
|
+
resultsChecksum
|
|
3044
|
+
});
|
|
3045
|
+
components = [{
|
|
3046
|
+
type: TargetType.Application,
|
|
3047
|
+
name: targetName
|
|
3048
|
+
}];
|
|
3049
|
+
}
|
|
1738
3050
|
return buildHdfResults({
|
|
1739
3051
|
generatorName: "sonarqube-to-hdf",
|
|
1740
3052
|
converterVersion: "1.0.0",
|
|
1741
3053
|
toolName: "SonarQube",
|
|
1742
3054
|
baselines,
|
|
1743
|
-
components
|
|
1744
|
-
type: Copyright.Application,
|
|
1745
|
-
name: projectKey
|
|
1746
|
-
})),
|
|
3055
|
+
components,
|
|
1747
3056
|
timestamp: /* @__PURE__ */ new Date()
|
|
1748
3057
|
});
|
|
1749
3058
|
}
|
|
3059
|
+
function deriveEmptyScanTarget(components) {
|
|
3060
|
+
if (components) {
|
|
3061
|
+
const project = components.find((c) => c.qualifier === "TRK");
|
|
3062
|
+
if (project) return project.key;
|
|
3063
|
+
if (components.length > 0) return components[0].key;
|
|
3064
|
+
}
|
|
3065
|
+
return "the SonarQube project";
|
|
3066
|
+
}
|
|
1750
3067
|
function convertProjectToBaseline(projectKey, issues, componentMap, ruleMap, resultsChecksum) {
|
|
1751
3068
|
const issuesByRule = /* @__PURE__ */ new Map();
|
|
1752
3069
|
for (const issue of issues) {
|
|
@@ -1786,7 +3103,11 @@ function convertRuleToRequirement(ruleKey, issues, componentMap, ruleMap) {
|
|
|
1786
3103
|
...allTags
|
|
1787
3104
|
} };
|
|
1788
3105
|
if (sourceLocation) options.sourceLocation = sourceLocation;
|
|
1789
|
-
|
|
3106
|
+
const req = createRequirement(ruleKey, title, [createDescription("default", description)], impact, results, options);
|
|
3107
|
+
const controlType = deriveControlTypeFromTags(nistControls);
|
|
3108
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
3109
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
3110
|
+
return req;
|
|
1790
3111
|
}
|
|
1791
3112
|
function extractDescription(rule) {
|
|
1792
3113
|
if (!rule) return "";
|
|
@@ -1905,7 +3226,21 @@ function buildResult(r) {
|
|
|
1905
3226
|
...startTime ? { startTime } : {}
|
|
1906
3227
|
});
|
|
1907
3228
|
}
|
|
1908
|
-
|
|
3229
|
+
/**
|
|
3230
|
+
* Synthesizes a single HDF result for a Config rule whose live evaluation
|
|
3231
|
+
* returned zero in-scope resources. The HDF schema requires `results` to have
|
|
3232
|
+
* minItems >= 1; this honestly signals to auditors that the rule's check ran
|
|
3233
|
+
* but had no scope in this account/region rather than vacuously claiming
|
|
3234
|
+
* "passed". See issue #80 bug 2.
|
|
3235
|
+
*/
|
|
3236
|
+
function buildNotApplicableResult(rule) {
|
|
3237
|
+
const codeDesc = `AWS Config rule ${rule.ConfigRuleName} evaluated zero in-scope resources in this account/region.`;
|
|
3238
|
+
return createResult(ResultStatus.NotApplicable, codeDesc, {
|
|
3239
|
+
codeDesc,
|
|
3240
|
+
startTime: /* @__PURE__ */ new Date()
|
|
3241
|
+
});
|
|
3242
|
+
}
|
|
3243
|
+
function buildRequirement$15(rule) {
|
|
1909
3244
|
const nist = buildNistTags$3(rule.Source.SourceIdentifier, rule.ConfigRuleName);
|
|
1910
3245
|
const tags = nist.length > 0 ? { nist } : {};
|
|
1911
3246
|
const descriptions = [{
|
|
@@ -1916,14 +3251,18 @@ function buildRequirement$14(rule) {
|
|
|
1916
3251
|
data: buildCheckText(rule)
|
|
1917
3252
|
}];
|
|
1918
3253
|
const title = `${getAccountId(rule.ConfigRuleArn)} - ${rule.ConfigRuleName}`;
|
|
1919
|
-
const results = rule.EvaluationResults.map(buildResult);
|
|
1920
|
-
|
|
3254
|
+
const results = rule.EvaluationResults.length > 0 ? rule.EvaluationResults.map(buildResult) : [buildNotApplicableResult(rule)];
|
|
3255
|
+
const req = createRequirement(rule.ConfigRuleId, title, descriptions, .5, results, {
|
|
1921
3256
|
tags,
|
|
1922
3257
|
sourceLocation: {
|
|
1923
3258
|
ref: rule.ConfigRuleArn,
|
|
1924
3259
|
line: 1
|
|
1925
3260
|
}
|
|
1926
3261
|
});
|
|
3262
|
+
const controlType = deriveControlTypeFromTags(nist);
|
|
3263
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
3264
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
3265
|
+
return req;
|
|
1927
3266
|
}
|
|
1928
3267
|
/**
|
|
1929
3268
|
* Converts an AWS Config static export (ConfigRulesFile JSON) to HDF format.
|
|
@@ -1942,7 +3281,7 @@ async function convertAwsConfigToHdf(input) {
|
|
|
1942
3281
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
1943
3282
|
if (truncatedRules) console.warn(`WARNING: Input truncated at ${limitedRules.length} ConfigRule items (original: ${data.ConfigRules.length})`);
|
|
1944
3283
|
const baseline = {
|
|
1945
|
-
...createMinimalBaseline("AWS Config", limitedRules.map(buildRequirement$
|
|
3284
|
+
...createMinimalBaseline("AWS Config", limitedRules.map(buildRequirement$15), { resultsChecksum }),
|
|
1946
3285
|
title: "AWS Config Compliance Results",
|
|
1947
3286
|
version: "1.0.0",
|
|
1948
3287
|
maintainer: "Amazon Web Services"
|
|
@@ -1956,7 +3295,7 @@ async function convertAwsConfigToHdf(input) {
|
|
|
1956
3295
|
toolName: "AWS Config",
|
|
1957
3296
|
baselines: [baseline],
|
|
1958
3297
|
components: [{
|
|
1959
|
-
type:
|
|
3298
|
+
type: TargetType.CloudAccount,
|
|
1960
3299
|
name: `AWS Account ${accountId}`,
|
|
1961
3300
|
labels: {
|
|
1962
3301
|
account: accountId,
|
|
@@ -1968,6 +3307,120 @@ async function convertAwsConfigToHdf(input) {
|
|
|
1968
3307
|
});
|
|
1969
3308
|
}
|
|
1970
3309
|
//#endregion
|
|
3310
|
+
//#region converters/checkov-to-hdf/typescript/converter.ts
|
|
3311
|
+
/**
|
|
3312
|
+
* Maps checkov result string to HDF ResultStatus.
|
|
3313
|
+
*/
|
|
3314
|
+
function mapStatus$2(result) {
|
|
3315
|
+
switch (result.toUpperCase()) {
|
|
3316
|
+
case "PASSED": return ResultStatus.Passed;
|
|
3317
|
+
case "FAILED": return ResultStatus.Failed;
|
|
3318
|
+
case "SKIPPED": return ResultStatus.NotReviewed;
|
|
3319
|
+
default: return ResultStatus.NotReviewed;
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
/**
|
|
3323
|
+
* Maps severity to impact, defaulting to 0.5 for null/unknown.
|
|
3324
|
+
*/
|
|
3325
|
+
function getImpact$8(severity) {
|
|
3326
|
+
if (!severity) return .5;
|
|
3327
|
+
return severityToImpact(severity);
|
|
3328
|
+
}
|
|
3329
|
+
/**
|
|
3330
|
+
* Converts a single CheckovCheck to an HDF RequirementResult.
|
|
3331
|
+
*/
|
|
3332
|
+
function checkToResult(check) {
|
|
3333
|
+
const status = mapStatus$2(check.check_result.result);
|
|
3334
|
+
const codeDesc = `Resource: ${check.resource}\nFile: ${check.file_path} (lines ${JSON.stringify(check.file_line_range)})`;
|
|
3335
|
+
let message;
|
|
3336
|
+
if (status === ResultStatus.NotReviewed && check.check_result.suppress_comment) message = check.check_result.suppress_comment;
|
|
3337
|
+
return createResult(status, message ?? "", { codeDesc });
|
|
3338
|
+
}
|
|
3339
|
+
/**
|
|
3340
|
+
* Converts a group of checks sharing a check_id into one EvaluatedRequirement.
|
|
3341
|
+
*/
|
|
3342
|
+
function buildRequirement$14(checkId, checks) {
|
|
3343
|
+
const rep = checks[0];
|
|
3344
|
+
const impact = getImpact$8(rep.severity);
|
|
3345
|
+
const tags = { nist: [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS] };
|
|
3346
|
+
const descriptions = [{
|
|
3347
|
+
label: "default",
|
|
3348
|
+
data: rep.check_name
|
|
3349
|
+
}];
|
|
3350
|
+
if (rep.guideline) descriptions.push({
|
|
3351
|
+
label: "check",
|
|
3352
|
+
data: rep.guideline
|
|
3353
|
+
});
|
|
3354
|
+
const results = checks.map(checkToResult);
|
|
3355
|
+
const req = createRequirement(checkId, rep.check_name, descriptions, impact, results, { tags });
|
|
3356
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
3357
|
+
const controlType = deriveControlTypeFromTags([...DEFAULT_STATIC_ANALYSIS_NIST_TAGS]);
|
|
3358
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
3359
|
+
return req;
|
|
3360
|
+
}
|
|
3361
|
+
/**
|
|
3362
|
+
* Parses checkov input which can be a single object or array.
|
|
3363
|
+
*/
|
|
3364
|
+
function parseInput(input) {
|
|
3365
|
+
const parsed = parseJSON(input);
|
|
3366
|
+
if (Array.isArray(parsed)) return parsed;
|
|
3367
|
+
if (!parsed || typeof parsed !== "object") throw new Error("Invalid checkov structure: not a valid JSON object");
|
|
3368
|
+
if (!parsed.results || typeof parsed.results !== "object") throw new Error("Invalid checkov structure: missing or invalid results field");
|
|
3369
|
+
return [parsed];
|
|
3370
|
+
}
|
|
3371
|
+
/**
|
|
3372
|
+
* Converts checkov output to HDF format.
|
|
3373
|
+
* Accepts native checkov JSON (single object or array) and SARIF format.
|
|
3374
|
+
* SARIF input is detected automatically and delegated to the shared SARIF converter.
|
|
3375
|
+
*
|
|
3376
|
+
* @param input - checkov JSON or SARIF string
|
|
3377
|
+
* @returns HDF JSON string
|
|
3378
|
+
*/
|
|
3379
|
+
async function convertCheckovToHdf(input) {
|
|
3380
|
+
validateInputSize(input, "checkov");
|
|
3381
|
+
registerAllFingerprints();
|
|
3382
|
+
const detected = detectConverter(input);
|
|
3383
|
+
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
|
|
3384
|
+
const resultsChecksum = await inputChecksum(input);
|
|
3385
|
+
const reports = parseInput(input);
|
|
3386
|
+
const allChecks = [];
|
|
3387
|
+
const checkTypes = [];
|
|
3388
|
+
let version;
|
|
3389
|
+
for (const report of reports) {
|
|
3390
|
+
checkTypes.push(report.check_type);
|
|
3391
|
+
if (!version && report.summary.checkov_version) version = report.summary.checkov_version;
|
|
3392
|
+
allChecks.push(...report.results.passed_checks);
|
|
3393
|
+
allChecks.push(...report.results.failed_checks);
|
|
3394
|
+
allChecks.push(...report.results.skipped_checks);
|
|
3395
|
+
}
|
|
3396
|
+
const { items: limitedChecks, truncated } = limitArray(allChecks);
|
|
3397
|
+
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
3398
|
+
if (truncated) console.warn(`WARNING: Input truncated at ${limitedChecks.length} check items (original: ${allChecks.length})`);
|
|
3399
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3400
|
+
for (const check of limitedChecks) {
|
|
3401
|
+
const existing = groups.get(check.check_id);
|
|
3402
|
+
if (existing) existing.push(check);
|
|
3403
|
+
else groups.set(check.check_id, [check]);
|
|
3404
|
+
}
|
|
3405
|
+
const requirements = [];
|
|
3406
|
+
for (const [checkId, checks] of groups) requirements.push(buildRequirement$14(checkId, checks));
|
|
3407
|
+
const format = checkTypes.join(", ");
|
|
3408
|
+
if (requirements.length === 0) {
|
|
3409
|
+
const target = format || "input";
|
|
3410
|
+
requirements.push(buildNoFindingsRequirement("checkov-no-findings", `Checkov scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
3411
|
+
}
|
|
3412
|
+
const baseline = createMinimalBaseline("Checkov Scan", requirements, { resultsChecksum });
|
|
3413
|
+
return buildHdfResults({
|
|
3414
|
+
generatorName: "checkov-to-hdf",
|
|
3415
|
+
converterVersion: "1.0.0",
|
|
3416
|
+
toolName: "Checkov",
|
|
3417
|
+
toolVersion: version,
|
|
3418
|
+
toolFormat: format,
|
|
3419
|
+
baselines: [baseline],
|
|
3420
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
3421
|
+
});
|
|
3422
|
+
}
|
|
3423
|
+
//#endregion
|
|
1971
3424
|
//#region converters/gosec-to-hdf/typescript/converter.ts
|
|
1972
3425
|
/**
|
|
1973
3426
|
* Severity to HDF impact mapping for gosec.
|
|
@@ -2010,8 +3463,9 @@ function issueToResult(issue) {
|
|
|
2010
3463
|
function buildRequirement$13(ruleId, issues) {
|
|
2011
3464
|
const rep = issues[0];
|
|
2012
3465
|
const impact = IMPACT_MAPPING$6[rep.severity.toUpperCase()] ?? .5;
|
|
3466
|
+
const nist = mapCWEToNIST([rep.cwe.id], DEFAULT_REMEDIATION_NIST_TAGS$1);
|
|
2013
3467
|
const tags = {
|
|
2014
|
-
nist
|
|
3468
|
+
nist,
|
|
2015
3469
|
cwe: {
|
|
2016
3470
|
id: rep.cwe.id,
|
|
2017
3471
|
url: rep.cwe.url
|
|
@@ -2025,7 +3479,11 @@ function buildRequirement$13(ruleId, issues) {
|
|
|
2025
3479
|
label: "check",
|
|
2026
3480
|
data: `CWE-${rep.cwe.id}: ${rep.cwe.url}`
|
|
2027
3481
|
}];
|
|
2028
|
-
|
|
3482
|
+
const req = createRequirement(ruleId, rep.details, descriptions, impact, results, { tags });
|
|
3483
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
3484
|
+
const controlType = deriveControlTypeFromTags(nist);
|
|
3485
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
3486
|
+
return req;
|
|
2029
3487
|
}
|
|
2030
3488
|
/**
|
|
2031
3489
|
* Converts gosec output to HDF format.
|
|
@@ -2055,6 +3513,7 @@ async function convertGosecToHdf(input) {
|
|
|
2055
3513
|
}
|
|
2056
3514
|
const requirements = [];
|
|
2057
3515
|
for (const [ruleId, issues] of groups) requirements.push(buildRequirement$13(ruleId, issues));
|
|
3516
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("gosec-no-findings", "gosec scanned Go codebase and reported zero findings.", /* @__PURE__ */ new Date()));
|
|
2058
3517
|
const baseline = createMinimalBaseline("gosec Scan", requirements, { resultsChecksum });
|
|
2059
3518
|
return buildHdfResults({
|
|
2060
3519
|
generatorName: "gosec-to-hdf",
|
|
@@ -2089,7 +3548,7 @@ function convertVulnToRequirement(vuln) {
|
|
|
2089
3548
|
const extras = {};
|
|
2090
3549
|
if (vuln.OSVDB && vuln.OSVDB !== "0") extras.osvdb = vuln.OSVDB;
|
|
2091
3550
|
const tags = buildNistCciTags(nistTags, cciTags, Object.keys(extras).length > 0 ? extras : void 0);
|
|
2092
|
-
|
|
3551
|
+
const req = {
|
|
2093
3552
|
id: vuln.id,
|
|
2094
3553
|
title: vuln.msg,
|
|
2095
3554
|
impact: .5,
|
|
@@ -2100,6 +3559,10 @@ function convertVulnToRequirement(vuln) {
|
|
|
2100
3559
|
data: vuln.msg
|
|
2101
3560
|
}]
|
|
2102
3561
|
};
|
|
3562
|
+
const controlType = deriveControlTypeFromTags(nistTags);
|
|
3563
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
3564
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
3565
|
+
return req;
|
|
2103
3566
|
}
|
|
2104
3567
|
async function convertNiktoToHdf(input) {
|
|
2105
3568
|
validateInputSize(input, "nikto");
|
|
@@ -2131,6 +3594,7 @@ async function convertNiktoToHdf(input) {
|
|
|
2131
3594
|
if (niktoData.host) targetParts.push(`Host: ${niktoData.host}`);
|
|
2132
3595
|
if (niktoData.port) targetParts.push(`Port: ${niktoData.port}`);
|
|
2133
3596
|
const targetName = targetParts.length > 0 ? targetParts.join(" ") : "Nikto Scan";
|
|
3597
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("nikto-no-findings", `Nikto scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
2134
3598
|
return buildHdfResults({
|
|
2135
3599
|
generatorName: "nikto-to-hdf",
|
|
2136
3600
|
converterVersion: "unknown",
|
|
@@ -2142,7 +3606,7 @@ async function convertNiktoToHdf(input) {
|
|
|
2142
3606
|
summary: niktoData.banner || ""
|
|
2143
3607
|
})],
|
|
2144
3608
|
components: [{
|
|
2145
|
-
type:
|
|
3609
|
+
type: TargetType.Application,
|
|
2146
3610
|
name: targetName
|
|
2147
3611
|
}]
|
|
2148
3612
|
});
|
|
@@ -2204,45 +3668,8 @@ async function convertZapToHdf(input) {
|
|
|
2204
3668
|
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input);
|
|
2205
3669
|
const resultsChecksum = await inputChecksum(input);
|
|
2206
3670
|
const zapData = parseJSON(input);
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
baselines: [createMinimalBaseline("OWASP ZAP Scan", [], {
|
|
2210
|
-
resultsChecksum,
|
|
2211
|
-
title: "OWASP ZAP Scan",
|
|
2212
|
-
summary: ""
|
|
2213
|
-
})],
|
|
2214
|
-
generator: {
|
|
2215
|
-
name: "zap-to-hdf",
|
|
2216
|
-
version: "unknown"
|
|
2217
|
-
},
|
|
2218
|
-
tool: {
|
|
2219
|
-
name: "OWASP ZAP",
|
|
2220
|
-
format: "JSON"
|
|
2221
|
-
}
|
|
2222
|
-
};
|
|
2223
|
-
return JSON.stringify(hdf, null, 2);
|
|
2224
|
-
}
|
|
2225
|
-
const site = selectSite(zapData.site);
|
|
2226
|
-
if (!site) {
|
|
2227
|
-
const hdf = {
|
|
2228
|
-
baselines: [createMinimalBaseline("OWASP ZAP Scan", [], {
|
|
2229
|
-
resultsChecksum,
|
|
2230
|
-
title: "OWASP ZAP Scan",
|
|
2231
|
-
summary: `ZAP Version ${zapData["@version"] ?? "unknown"}`
|
|
2232
|
-
})],
|
|
2233
|
-
generator: {
|
|
2234
|
-
name: "zap-to-hdf",
|
|
2235
|
-
version: "unknown"
|
|
2236
|
-
},
|
|
2237
|
-
tool: {
|
|
2238
|
-
name: "OWASP ZAP",
|
|
2239
|
-
format: "JSON"
|
|
2240
|
-
}
|
|
2241
|
-
};
|
|
2242
|
-
if (zapData["@generated"]) hdf.timestamp = new Date(zapData["@generated"]);
|
|
2243
|
-
return JSON.stringify(hdf, null, 2);
|
|
2244
|
-
}
|
|
2245
|
-
const alerts = site.alerts ?? [];
|
|
3671
|
+
const site = selectSite(Array.isArray(zapData.site) ? zapData.site : []);
|
|
3672
|
+
const alerts = site?.alerts ?? [];
|
|
2246
3673
|
const pluginIdCount = /* @__PURE__ */ new Map();
|
|
2247
3674
|
const { items: limitedAlerts, truncated } = limitArray(alerts);
|
|
2248
3675
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
@@ -2291,14 +3718,24 @@ async function convertZapToHdf(input) {
|
|
|
2291
3718
|
impact,
|
|
2292
3719
|
results,
|
|
2293
3720
|
tags,
|
|
2294
|
-
descriptions
|
|
3721
|
+
descriptions,
|
|
3722
|
+
verificationMethod: VerificationMethodEnum.Automated
|
|
2295
3723
|
};
|
|
3724
|
+
const controlType = deriveControlTypeFromTags(nistTags);
|
|
3725
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
2296
3726
|
requirements.push(req);
|
|
2297
3727
|
}
|
|
2298
|
-
const targetName = site["@host"] ?? "Unknown Host";
|
|
3728
|
+
const targetName = site?.["@host"] ?? "Unknown Host";
|
|
3729
|
+
const siteName = site?.["@name"] ?? "";
|
|
3730
|
+
const baselineName = site && (site["@name"] || site["@host"]) ? `OWASP ZAP Scan of ${site["@name"] ?? targetName}` : "OWASP ZAP Scan";
|
|
3731
|
+
if (requirements.length === 0) {
|
|
3732
|
+
let target = siteName || targetName;
|
|
3733
|
+
if (!target || target === "Unknown Host") target = "the target site";
|
|
3734
|
+
requirements.push(buildNoFindingsRequirement("zap-no-findings", `OWASP ZAP scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
3735
|
+
}
|
|
2299
3736
|
const baseline = createMinimalBaseline("OWASP ZAP Scan", requirements, {
|
|
2300
3737
|
resultsChecksum,
|
|
2301
|
-
title:
|
|
3738
|
+
title: baselineName,
|
|
2302
3739
|
summary: `ZAP Version ${zapData["@version"] ?? "unknown"}`
|
|
2303
3740
|
});
|
|
2304
3741
|
const tool = {
|
|
@@ -2307,14 +3744,14 @@ async function convertZapToHdf(input) {
|
|
|
2307
3744
|
};
|
|
2308
3745
|
if (zapData["@version"]) tool.version = zapData["@version"];
|
|
2309
3746
|
const components = [];
|
|
2310
|
-
if (site["@name"]) components.push({
|
|
3747
|
+
if (site?.["@name"]) components.push({
|
|
2311
3748
|
name: targetName,
|
|
2312
|
-
type:
|
|
3749
|
+
type: TargetType.Application,
|
|
2313
3750
|
url: site["@name"]
|
|
2314
3751
|
});
|
|
2315
3752
|
else if (targetName !== "Unknown Host") components.push({
|
|
2316
3753
|
name: targetName,
|
|
2317
|
-
type:
|
|
3754
|
+
type: TargetType.Application
|
|
2318
3755
|
});
|
|
2319
3756
|
const hdf = {
|
|
2320
3757
|
baselines: [baseline],
|
|
@@ -2435,7 +3872,10 @@ async function convertCyclonedxToHdf(input) {
|
|
|
2435
3872
|
const affects = vuln.affects ?? [];
|
|
2436
3873
|
const results = affects.length > 0 ? affects.map((affect) => createResult(ResultStatus.Failed, void 0, { codeDesc: formatCodeDesc$4(componentLookup, affect.ref) })) : [createResult(ResultStatus.Failed, void 0, { codeDesc: `Vulnerability ${vuln.id}` })];
|
|
2437
3874
|
const title = vuln.source?.name ? `${vuln.id} (${vuln.source.name})` : vuln.id;
|
|
2438
|
-
|
|
3875
|
+
const req = createRequirement(vuln.id, title, descriptions, impact, results, { tags });
|
|
3876
|
+
const controlType = deriveControlTypeFromTags(nist);
|
|
3877
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
3878
|
+
requirements.push(req);
|
|
2439
3879
|
}
|
|
2440
3880
|
const baseline = createMinimalBaseline("CycloneDX Scan", requirements, { resultsChecksum });
|
|
2441
3881
|
const targetName = bom.metadata?.component?.name ?? "";
|
|
@@ -2447,7 +3887,7 @@ async function convertCyclonedxToHdf(input) {
|
|
|
2447
3887
|
baselines: [baseline],
|
|
2448
3888
|
components: [{
|
|
2449
3889
|
name: targetName,
|
|
2450
|
-
type:
|
|
3890
|
+
type: TargetType.Application
|
|
2451
3891
|
}],
|
|
2452
3892
|
timestamp: /* @__PURE__ */ new Date()
|
|
2453
3893
|
});
|
|
@@ -2501,6 +3941,9 @@ function createRow(baseline, requirement, target) {
|
|
|
2501
3941
|
"Status": String(status),
|
|
2502
3942
|
"NIST Controls": nistControls,
|
|
2503
3943
|
"CCI Controls": cciControls,
|
|
3944
|
+
"Control Type": requirement.controlType ?? "",
|
|
3945
|
+
"Verification Method": requirement.verificationMethod ?? "",
|
|
3946
|
+
"Applicability": requirement.applicability ?? "",
|
|
2504
3947
|
"Result Message": message
|
|
2505
3948
|
};
|
|
2506
3949
|
}
|
|
@@ -2624,7 +4067,12 @@ async function convertSplunkToHdf(input) {
|
|
|
2624
4067
|
})) : [];
|
|
2625
4068
|
const options = { tags: control.tags ?? {} };
|
|
2626
4069
|
if (control.source_location) options.sourceLocation = control.source_location;
|
|
2627
|
-
|
|
4070
|
+
const req = createRequirement(control.id, control.title, descriptions, control.impact, results, options);
|
|
4071
|
+
const nistTagsRaw = control.tags?.["nist"];
|
|
4072
|
+
const controlType = deriveControlTypeFromTags(Array.isArray(nistTagsRaw) ? nistTagsRaw.filter((t) => typeof t === "string") : []);
|
|
4073
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
4074
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
4075
|
+
return req;
|
|
2628
4076
|
});
|
|
2629
4077
|
const groups = convertGroups(profile.groups);
|
|
2630
4078
|
const baselineOptions = { resultsChecksum };
|
|
@@ -2639,7 +4087,7 @@ async function convertSplunkToHdf(input) {
|
|
|
2639
4087
|
baselines: allBaselines,
|
|
2640
4088
|
components: [{
|
|
2641
4089
|
name: targetName,
|
|
2642
|
-
type:
|
|
4090
|
+
type: TargetType.Host,
|
|
2643
4091
|
osName: targetRelease || void 0,
|
|
2644
4092
|
labels: {}
|
|
2645
4093
|
}],
|
|
@@ -2739,9 +4187,9 @@ function gitlabSeverityToImpact(severity) {
|
|
|
2739
4187
|
}
|
|
2740
4188
|
function scanTypeToTargetType(scanType) {
|
|
2741
4189
|
switch (scanType) {
|
|
2742
|
-
case "dast": return
|
|
2743
|
-
case "container_scanning": return
|
|
2744
|
-
default: return
|
|
4190
|
+
case "dast": return TargetType.Application;
|
|
4191
|
+
case "container_scanning": return TargetType.ContainerImage;
|
|
4192
|
+
default: return TargetType.Repository;
|
|
2745
4193
|
}
|
|
2746
4194
|
}
|
|
2747
4195
|
function scanTypeLabel(scanType) {
|
|
@@ -2867,13 +4315,21 @@ async function convertGitlabToHdf(input) {
|
|
|
2867
4315
|
impact,
|
|
2868
4316
|
results: [result],
|
|
2869
4317
|
tags,
|
|
2870
|
-
descriptions
|
|
4318
|
+
descriptions,
|
|
4319
|
+
verificationMethod: VerificationMethodEnum.Automated
|
|
2871
4320
|
};
|
|
4321
|
+
const controlType = deriveControlTypeFromTags(nistTags);
|
|
4322
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
2872
4323
|
requirements.push(req);
|
|
2873
4324
|
}
|
|
4325
|
+
const label = scanTypeLabel(scanType);
|
|
4326
|
+
if (requirements.length === 0) {
|
|
4327
|
+
const ts = startTime ? new Date(startTime) : /* @__PURE__ */ new Date();
|
|
4328
|
+
requirements.push(buildNoFindingsRequirement("gitlab-no-findings", `GitLab ${label} scan via ${scannerName} reported zero findings.`, ts));
|
|
4329
|
+
}
|
|
2874
4330
|
const baseline = createMinimalBaseline("GitLab Security Scan", requirements, {
|
|
2875
4331
|
resultsChecksum,
|
|
2876
|
-
title: `GitLab ${
|
|
4332
|
+
title: `GitLab ${label} Security Scan`,
|
|
2877
4333
|
summary: `Scanner: ${scannerName}${scannerVersion ? ` v${scannerVersion}` : ""}`
|
|
2878
4334
|
});
|
|
2879
4335
|
const tool = {
|
|
@@ -3004,13 +4460,17 @@ function buildRequirement$12(reqID, findings) {
|
|
|
3004
4460
|
}));
|
|
3005
4461
|
const sourceFile = getSourceFile(rep);
|
|
3006
4462
|
const sourceLine = getSourceLine(rep);
|
|
3007
|
-
|
|
4463
|
+
const req = createRequirement(reqID, title, descriptions, .5, results, {
|
|
3008
4464
|
tags,
|
|
3009
4465
|
sourceLocation: sourceFile ? {
|
|
3010
4466
|
ref: sourceFile,
|
|
3011
4467
|
line: sourceLine
|
|
3012
4468
|
} : void 0
|
|
3013
4469
|
});
|
|
4470
|
+
const controlType = deriveControlTypeFromTags(TRUFFLEHOG_NIST);
|
|
4471
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
4472
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
4473
|
+
return req;
|
|
3014
4474
|
}
|
|
3015
4475
|
/**
|
|
3016
4476
|
* Converts TruffleHog output to HDF format.
|
|
@@ -3023,19 +4483,22 @@ async function convertTrufflehogToHdf(input) {
|
|
|
3023
4483
|
if (!input || input.trim().length === 0) throw new Error("trufflehog: empty input");
|
|
3024
4484
|
validateInputSize(input, "trufflehog");
|
|
3025
4485
|
const findings = parseFindings(input);
|
|
3026
|
-
if (findings.length === 0) throw new Error("trufflehog: no findings in input");
|
|
3027
4486
|
const resultsChecksum = await inputChecksum(input);
|
|
3028
4487
|
const limitedFindings = limitArrayWithWarning(findings, "finding");
|
|
3029
4488
|
const groups = groupFindings(limitedFindings);
|
|
3030
4489
|
const requirements = [];
|
|
3031
4490
|
for (const [reqID, group] of groups) requirements.push(buildRequirement$12(reqID, group));
|
|
4491
|
+
if (requirements.length === 0) {
|
|
4492
|
+
const target = findGitRepoURL(limitedFindings) ?? limitedFindings[0]?.SourceName ?? "the target source";
|
|
4493
|
+
requirements.push(buildNoFindingsRequirement("trufflehog-no-findings", `TruffleHog scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
4494
|
+
}
|
|
3032
4495
|
const hdf = {
|
|
3033
4496
|
baselines: [createMinimalBaseline("TruffleHog Scan", requirements, {
|
|
3034
4497
|
resultsChecksum,
|
|
3035
4498
|
title: `TruffleHog Scan (${limitedFindings[0]?.SourceName ?? "trufflehog"})`
|
|
3036
4499
|
})],
|
|
3037
4500
|
generator: {
|
|
3038
|
-
name: "hdf
|
|
4501
|
+
name: "trufflehog-to-hdf",
|
|
3039
4502
|
version: "1.0.0"
|
|
3040
4503
|
},
|
|
3041
4504
|
tool: {
|
|
@@ -3047,7 +4510,7 @@ async function convertTrufflehogToHdf(input) {
|
|
|
3047
4510
|
const repoURL = findGitRepoURL(limitedFindings);
|
|
3048
4511
|
if (repoURL) hdf.components = [{
|
|
3049
4512
|
name: repoURL,
|
|
3050
|
-
type:
|
|
4513
|
+
type: TargetType.Repository
|
|
3051
4514
|
}];
|
|
3052
4515
|
return JSON.stringify(hdf, null, 2);
|
|
3053
4516
|
}
|
|
@@ -3109,6 +4572,7 @@ async function convertBurpsuiteToHdf(input) {
|
|
|
3109
4572
|
const requirements = [];
|
|
3110
4573
|
for (const [issueType, groupIssues] of groups) requirements.push(buildRequirement$11(issueType, groupIssues));
|
|
3111
4574
|
const targetName = limitedIssues.length > 0 ? (limitedIssues[0].host?.["#text"] ?? "Unknown").trim() : "Unknown";
|
|
4575
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("burpsuite-no-findings", `Burp Suite scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
3112
4576
|
const baseline = createMinimalBaseline("BurpSuite Scan", requirements, {
|
|
3113
4577
|
resultsChecksum,
|
|
3114
4578
|
title: `BurpSuite Scan: ${targetName}`
|
|
@@ -3122,7 +4586,7 @@ async function convertBurpsuiteToHdf(input) {
|
|
|
3122
4586
|
baselines: [baseline],
|
|
3123
4587
|
components: [{
|
|
3124
4588
|
name: targetName,
|
|
3125
|
-
type:
|
|
4589
|
+
type: TargetType.Application
|
|
3126
4590
|
}],
|
|
3127
4591
|
generator: {
|
|
3128
4592
|
name: "burpsuite-to-hdf",
|
|
@@ -3165,14 +4629,18 @@ function buildRequirement$11(issueType, issues) {
|
|
|
3165
4629
|
};
|
|
3166
4630
|
});
|
|
3167
4631
|
const impact = getImpact$7(rep.severity ?? "information");
|
|
3168
|
-
|
|
4632
|
+
const req = {
|
|
3169
4633
|
id: issueType,
|
|
3170
4634
|
title: rep.name ?? void 0,
|
|
3171
4635
|
impact,
|
|
3172
4636
|
tags,
|
|
3173
4637
|
descriptions,
|
|
3174
|
-
results
|
|
4638
|
+
results,
|
|
4639
|
+
verificationMethod: VerificationMethodEnum.Automated
|
|
3175
4640
|
};
|
|
4641
|
+
const controlType = deriveControlTypeFromTags(nist);
|
|
4642
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
4643
|
+
return req;
|
|
3176
4644
|
}
|
|
3177
4645
|
//#endregion
|
|
3178
4646
|
//#region converters/dbprotect-to-hdf/typescript/converter.ts
|
|
@@ -3261,7 +4729,11 @@ function buildRequirement$10(checkID, findings, hasStatus) {
|
|
|
3261
4729
|
startTime: parseDate(f["Date"] ?? "")
|
|
3262
4730
|
});
|
|
3263
4731
|
});
|
|
3264
|
-
|
|
4732
|
+
const req = createRequirement(checkID, rep["Check"] ?? "", descriptions, getImpact$6(rep["Risk DV"] ?? ""), results, { tags });
|
|
4733
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
4734
|
+
const controlType = deriveControlTypeFromTags([...nist]);
|
|
4735
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
4736
|
+
return req;
|
|
3265
4737
|
}
|
|
3266
4738
|
/**
|
|
3267
4739
|
* Converts DBProtect Cognos XML output to HDF format.
|
|
@@ -3311,7 +4783,7 @@ async function convertDbprotectToHdf(input) {
|
|
|
3311
4783
|
})],
|
|
3312
4784
|
components: [{
|
|
3313
4785
|
name: targetName,
|
|
3314
|
-
type:
|
|
4786
|
+
type: TargetType.Host
|
|
3315
4787
|
}],
|
|
3316
4788
|
timestamp: /* @__PURE__ */ new Date()
|
|
3317
4789
|
});
|
|
@@ -3351,6 +4823,148 @@ function buildSummary(result) {
|
|
|
3351
4823
|
return `Package Vulnerability Summary: ${result.vulnerabilityDistribution ? String(result.vulnerabilityDistribution.total) : "N/A"} Application Compliance Issue Total: ${result.complianceDistribution ? String(result.complianceDistribution.total) : "N/A"}`;
|
|
3352
4824
|
}
|
|
3353
4825
|
/**
|
|
4826
|
+
* Detects CVSS schema version enum from the vector prefix. Defaults to 3.1
|
|
4827
|
+
* since modern Twistlock exclusively emits 3.x output.
|
|
4828
|
+
*/
|
|
4829
|
+
function cvssVersionFromVector(vector) {
|
|
4830
|
+
if (!vector) return Version.The31;
|
|
4831
|
+
if (vector.startsWith("CVSS:2.0/")) return Version.The20;
|
|
4832
|
+
if (vector.startsWith("CVSS:3.0/")) return Version.The30;
|
|
4833
|
+
if (vector.startsWith("CVSS:4.0/")) return Version.The40;
|
|
4834
|
+
return Version.The31;
|
|
4835
|
+
}
|
|
4836
|
+
/**
|
|
4837
|
+
* Maps cvssScoreToSeverity('low'|'medium'|...) into the CVSSSeverity enum.
|
|
4838
|
+
*/
|
|
4839
|
+
function cvssSeverityFromScore(score) {
|
|
4840
|
+
switch (cvssScoreToSeverity(score)) {
|
|
4841
|
+
case "none": return CVSSSeverity.None;
|
|
4842
|
+
case "low": return CVSSSeverity.Low;
|
|
4843
|
+
case "medium": return CVSSSeverity.Medium;
|
|
4844
|
+
case "high": return CVSSSeverity.High;
|
|
4845
|
+
case "critical": return CVSSSeverity.Critical;
|
|
4846
|
+
default: return;
|
|
4847
|
+
}
|
|
4848
|
+
}
|
|
4849
|
+
/**
|
|
4850
|
+
* Builds a Cvss entry from a Twistlock vulnerability. Returns undefined only
|
|
4851
|
+
* when neither a score nor a vector is available. When the vendor emits a
|
|
4852
|
+
* score but no vector (common in Twistlock/Prisma Cloud output), the Cvss
|
|
4853
|
+
* entry is still emitted — the schema makes baseVector optional precisely
|
|
4854
|
+
* so vendor-final-score data isn't dropped.
|
|
4855
|
+
*/
|
|
4856
|
+
function buildCvss(vuln) {
|
|
4857
|
+
const hasScore = typeof vuln.cvss === "number" && Number.isFinite(vuln.cvss);
|
|
4858
|
+
const hasVector = !!vuln.vector;
|
|
4859
|
+
if (!hasScore && !hasVector) return void 0;
|
|
4860
|
+
const cv = { version: cvssVersionFromVector(vuln.vector) };
|
|
4861
|
+
if (hasScore) {
|
|
4862
|
+
cv.baseScore = vuln.cvss;
|
|
4863
|
+
const sev = cvssSeverityFromScore(vuln.cvss);
|
|
4864
|
+
if (sev !== void 0) cv.baseSeverity = sev;
|
|
4865
|
+
}
|
|
4866
|
+
if (hasVector) cv.baseVector = vuln.vector;
|
|
4867
|
+
const source = vuln.cve ?? vuln.id;
|
|
4868
|
+
if (source) cv.source = source;
|
|
4869
|
+
return cv;
|
|
4870
|
+
}
|
|
4871
|
+
const CWE_REGEX = /cwe[-_]?(\d+)/gi;
|
|
4872
|
+
/**
|
|
4873
|
+
* Extracts canonical CWE-N identifiers from a free-form string.
|
|
4874
|
+
*/
|
|
4875
|
+
function parseCwes(raw) {
|
|
4876
|
+
if (!raw) return [];
|
|
4877
|
+
const out = [];
|
|
4878
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4879
|
+
for (const m of raw.matchAll(CWE_REGEX)) {
|
|
4880
|
+
const id = `CWE-${m[1]}`;
|
|
4881
|
+
if (seen.has(id)) continue;
|
|
4882
|
+
seen.add(id);
|
|
4883
|
+
out.push(id);
|
|
4884
|
+
}
|
|
4885
|
+
return out;
|
|
4886
|
+
}
|
|
4887
|
+
function rhelDistro(distro) {
|
|
4888
|
+
const low = distro.toLowerCase();
|
|
4889
|
+
return [
|
|
4890
|
+
"red hat",
|
|
4891
|
+
"rhel",
|
|
4892
|
+
"centos",
|
|
4893
|
+
"fedora",
|
|
4894
|
+
"amazon linux",
|
|
4895
|
+
"oracle linux",
|
|
4896
|
+
"rocky",
|
|
4897
|
+
"alma"
|
|
4898
|
+
].some((m) => low.includes(m));
|
|
4899
|
+
}
|
|
4900
|
+
function debDistro(distro) {
|
|
4901
|
+
const low = distro.toLowerCase();
|
|
4902
|
+
return ["debian", "ubuntu"].some((m) => low.includes(m));
|
|
4903
|
+
}
|
|
4904
|
+
/**
|
|
4905
|
+
* Maps a Twistlock package type plus the result's distro string to a schema
|
|
4906
|
+
* Ecosystem value. Defaults to 'generic' for unknown types.
|
|
4907
|
+
*/
|
|
4908
|
+
function resolveEcosystem(packageType, distro) {
|
|
4909
|
+
const t = (packageType ?? "").toLowerCase();
|
|
4910
|
+
const d = distro ?? "";
|
|
4911
|
+
switch (t) {
|
|
4912
|
+
case "os":
|
|
4913
|
+
if (rhelDistro(d)) return Ecosystem.RPM;
|
|
4914
|
+
if (debDistro(d)) return Ecosystem.Deb;
|
|
4915
|
+
return Ecosystem.Generic;
|
|
4916
|
+
case "rpm": return Ecosystem.RPM;
|
|
4917
|
+
case "deb": return Ecosystem.Deb;
|
|
4918
|
+
case "jar":
|
|
4919
|
+
case "maven": return Ecosystem.Maven;
|
|
4920
|
+
case "python":
|
|
4921
|
+
case "pypi": return Ecosystem.Pypi;
|
|
4922
|
+
case "nodejs":
|
|
4923
|
+
case "npm": return Ecosystem.Npm;
|
|
4924
|
+
case "gem": return Ecosystem.Gem;
|
|
4925
|
+
case "nuget": return Ecosystem.Nuget;
|
|
4926
|
+
case "go": return Ecosystem.Go;
|
|
4927
|
+
case "cargo": return Ecosystem.Cargo;
|
|
4928
|
+
default: return Ecosystem.Generic;
|
|
4929
|
+
}
|
|
4930
|
+
}
|
|
4931
|
+
const FIX_VERSION_REGEX = /\d+(?:\.\d+)+[A-Za-z0-9._+\-]*/;
|
|
4932
|
+
/**
|
|
4933
|
+
* Extracts the first version-looking token from fixedBy or a "fixed in X"
|
|
4934
|
+
* status string. Returns empty string when no fix info is present.
|
|
4935
|
+
*/
|
|
4936
|
+
function extractFixedInVersion(vuln) {
|
|
4937
|
+
if (vuln.fixedBy) return vuln.fixedBy;
|
|
4938
|
+
if (!(vuln.status ?? "").toLowerCase().includes("fixed")) return "";
|
|
4939
|
+
const m = (vuln.status ?? "").match(FIX_VERSION_REGEX);
|
|
4940
|
+
return m ? m[0] : "";
|
|
4941
|
+
}
|
|
4942
|
+
/**
|
|
4943
|
+
* Builds an AffectedPackage entry from per-vulnerability fields. Returns
|
|
4944
|
+
* undefined when packageName or packageVersion are missing (both required).
|
|
4945
|
+
*/
|
|
4946
|
+
function buildAffectedPackage(vuln, packageTypes, distro) {
|
|
4947
|
+
if (!vuln.packageName || !vuln.packageVersion) return void 0;
|
|
4948
|
+
const pkgType = vuln.packageType ?? packageTypes.get(vuln.packageName);
|
|
4949
|
+
const pkg = {
|
|
4950
|
+
name: vuln.packageName,
|
|
4951
|
+
version: vuln.packageVersion,
|
|
4952
|
+
ecosystem: resolveEcosystem(pkgType, distro)
|
|
4953
|
+
};
|
|
4954
|
+
const fixed = extractFixedInVersion(vuln);
|
|
4955
|
+
if (fixed) pkg.fixedInVersion = fixed;
|
|
4956
|
+
return pkg;
|
|
4957
|
+
}
|
|
4958
|
+
/**
|
|
4959
|
+
* Indexes packageName → packageType from the result-level packages array.
|
|
4960
|
+
*/
|
|
4961
|
+
function buildPackageTypeIndex(pkgs) {
|
|
4962
|
+
const idx = /* @__PURE__ */ new Map();
|
|
4963
|
+
if (!pkgs) return idx;
|
|
4964
|
+
for (const p of pkgs) if (p.name && p.type) idx.set(p.name, p.type);
|
|
4965
|
+
return idx;
|
|
4966
|
+
}
|
|
4967
|
+
/**
|
|
3354
4968
|
* Builds the code_desc string for a vulnerability result.
|
|
3355
4969
|
*/
|
|
3356
4970
|
function formatCodeDesc$2(vuln) {
|
|
@@ -3360,10 +4974,17 @@ function formatCodeDesc$2(vuln) {
|
|
|
3360
4974
|
}
|
|
3361
4975
|
/**
|
|
3362
4976
|
* Converts a single vulnerability into an EvaluatedRequirement.
|
|
4977
|
+
*
|
|
4978
|
+
* @param vuln - The Twistlock vulnerability object
|
|
4979
|
+
* @param packageTypes - name→type lookup built from the enclosing result's packages array
|
|
4980
|
+
* @param distro - the enclosing result's distro string, used to disambiguate "os" packages
|
|
3363
4981
|
*/
|
|
3364
|
-
function buildRequirement$9(vuln) {
|
|
4982
|
+
function buildRequirement$9(vuln, packageTypes, distro) {
|
|
3365
4983
|
const nist = DEFAULT_REMEDIATION_NIST_TAGS;
|
|
3366
|
-
const
|
|
4984
|
+
const cciTags = nistToCci(nist);
|
|
4985
|
+
const extras = { cveid: [vuln.id] };
|
|
4986
|
+
if (typeof vuln.cvss === "number" && vuln.cvss > 0) extras["cvss_base_score"] = vuln.cvss;
|
|
4987
|
+
const tags = buildNistCciTags(nist, cciTags, extras);
|
|
3367
4988
|
const descriptions = [{
|
|
3368
4989
|
label: "default",
|
|
3369
4990
|
data: vuln.description
|
|
@@ -3373,7 +4994,17 @@ function buildRequirement$9(vuln) {
|
|
|
3373
4994
|
codeDesc: formatCodeDesc$2(vuln),
|
|
3374
4995
|
startTime
|
|
3375
4996
|
})];
|
|
3376
|
-
|
|
4997
|
+
const req = createRequirement(vuln.id, vuln.id, descriptions, twistlockSeverityToImpact(vuln.severity), results, { tags });
|
|
4998
|
+
const controlType = deriveControlTypeFromTags(nist);
|
|
4999
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
5000
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
5001
|
+
const cv = buildCvss(vuln);
|
|
5002
|
+
if (cv) req.cvss = [cv];
|
|
5003
|
+
const cwes = parseCwes(vuln.cwe);
|
|
5004
|
+
if (cwes.length > 0) req.cwe = cwes;
|
|
5005
|
+
const pkg = buildAffectedPackage(vuln, packageTypes, distro);
|
|
5006
|
+
if (pkg) req.affectedPackages = [pkg];
|
|
5007
|
+
return req;
|
|
3377
5008
|
}
|
|
3378
5009
|
/**
|
|
3379
5010
|
* Converts a single TwistlockResult to an EvaluatedBaseline.
|
|
@@ -3383,7 +5014,13 @@ function convertSingleResult(result, resultsChecksum) {
|
|
|
3383
5014
|
const { items: limitedVulns, truncated } = limitArray(vulns);
|
|
3384
5015
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
3385
5016
|
if (truncated) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${vulns.length})`);
|
|
3386
|
-
|
|
5017
|
+
const packageTypes = buildPackageTypeIndex(result.packages);
|
|
5018
|
+
const requirements = limitedVulns.map((vuln) => buildRequirement$9(vuln, packageTypes, result.distro));
|
|
5019
|
+
if (requirements.length === 0) {
|
|
5020
|
+
const target = result.name ?? result.repository ?? result.id ?? "scan target";
|
|
5021
|
+
requirements.push(buildNoFindingsRequirement("twistlock-no-findings", `Twistlock scanned ${target} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
|
|
5022
|
+
}
|
|
5023
|
+
return createMinimalBaseline("Twistlock Scan", requirements, {
|
|
3387
5024
|
resultsChecksum,
|
|
3388
5025
|
title: buildTitle$1(result),
|
|
3389
5026
|
summary: buildSummary(result)
|
|
@@ -3418,7 +5055,7 @@ async function convertTwistlockToHdf(input) {
|
|
|
3418
5055
|
baselines,
|
|
3419
5056
|
components: [{
|
|
3420
5057
|
name: targetName,
|
|
3421
|
-
type:
|
|
5058
|
+
type: TargetType.ContainerImage,
|
|
3422
5059
|
labels: { image: results[0]?.id ?? targetName }
|
|
3423
5060
|
}],
|
|
3424
5061
|
timestamp: /* @__PURE__ */ new Date()
|
|
@@ -3486,7 +5123,32 @@ function buildRequirement$8(finding, timestamp) {
|
|
|
3486
5123
|
codeDesc,
|
|
3487
5124
|
startTime: timestamp ? new Date(timestamp) : void 0
|
|
3488
5125
|
})];
|
|
3489
|
-
|
|
5126
|
+
const req = createRequirement(finding.matrix, getTitle$1(finding), descriptions, getImpact$5(finding.vulnerability.severity), results, { tags });
|
|
5127
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
5128
|
+
const controlType = deriveControlTypeFromTags(nist);
|
|
5129
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
5130
|
+
const pkg = buildAffectedPackageFromComponent(finding.component);
|
|
5131
|
+
if (pkg) req.affectedPackages = [pkg];
|
|
5132
|
+
return req;
|
|
5133
|
+
}
|
|
5134
|
+
/**
|
|
5135
|
+
* Builds an Affected_Package from a Dependency-Track component. Prefers
|
|
5136
|
+
* the rich identifiers Dependency-Track already exposes (purl, cpe) and
|
|
5137
|
+
* augments with name/version/ecosystem when available. Returns undefined
|
|
5138
|
+
* when the component carries no schema-acceptable identifier.
|
|
5139
|
+
*/
|
|
5140
|
+
function buildAffectedPackageFromComponent(c) {
|
|
5141
|
+
let ecosystem;
|
|
5142
|
+
if (c.purl) ecosystem = ecosystemFromPurlType(parsePurl(c.purl)?.type);
|
|
5143
|
+
else if (c.name && c.version) ecosystem = ecosystemFromPurlType(void 0);
|
|
5144
|
+
return buildAffectedPackage$1({
|
|
5145
|
+
name: c.name,
|
|
5146
|
+
version: c.version,
|
|
5147
|
+
ecosystem,
|
|
5148
|
+
purl: c.purl,
|
|
5149
|
+
cpe: c.cpe,
|
|
5150
|
+
fixedInVersion: c.latestVersion
|
|
5151
|
+
});
|
|
3490
5152
|
}
|
|
3491
5153
|
/**
|
|
3492
5154
|
* Converts Dependency-Track FPF JSON output to HDF format.
|
|
@@ -3501,7 +5163,12 @@ async function convertDeptrackToHdf(input) {
|
|
|
3501
5163
|
const parsed = parseJSON(input);
|
|
3502
5164
|
if (!parsed || typeof parsed !== "object") throw new Error("deptrack: invalid JSON");
|
|
3503
5165
|
if (!parsed.findings && !parsed.project && !parsed.meta) throw new Error("deptrack: input does not appear to be a Dependency-Track report");
|
|
3504
|
-
const
|
|
5166
|
+
const requirements = (parsed.findings ?? []).map((finding) => buildRequirement$8(finding, parsed.meta?.timestamp));
|
|
5167
|
+
if (requirements.length === 0) {
|
|
5168
|
+
const projectName = parsed.project?.name ?? parsed.project?.uuid ?? "";
|
|
5169
|
+
requirements.push(buildNoFindingsRequirement("deptrack-no-findings", `Dependency-Track analyzed ${projectName} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
|
|
5170
|
+
}
|
|
5171
|
+
const baseline = createMinimalBaseline("Dependency-Track Scan", requirements, {
|
|
3505
5172
|
resultsChecksum,
|
|
3506
5173
|
title: `Dependency-Track: ${parsed.project?.name ?? ""} ${parsed.project?.version ?? ""}`,
|
|
3507
5174
|
summary: parsed.project?.description
|
|
@@ -3515,7 +5182,7 @@ async function convertDeptrackToHdf(input) {
|
|
|
3515
5182
|
baselines: [baseline],
|
|
3516
5183
|
components: [{
|
|
3517
5184
|
name: targetName,
|
|
3518
|
-
type:
|
|
5185
|
+
type: TargetType.Application
|
|
3519
5186
|
}],
|
|
3520
5187
|
timestamp: /* @__PURE__ */ new Date()
|
|
3521
5188
|
});
|
|
@@ -3589,7 +5256,52 @@ function buildRequirement$7(entryID, entries) {
|
|
|
3589
5256
|
data: formatDescription(rep)
|
|
3590
5257
|
}];
|
|
3591
5258
|
const results = entries.map((entry) => createResult(ResultStatus.Failed, void 0, { codeDesc: formatCodeDesc$1(entry) }));
|
|
3592
|
-
|
|
5259
|
+
const req = createRequirement(entryID, rep.summary, descriptions, severityToImpact(rep.severity), results, { tags });
|
|
5260
|
+
const controlType = deriveControlTypeFromTags(nist);
|
|
5261
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
5262
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
5263
|
+
const pkg = buildAffectedPackageFromEntry(rep);
|
|
5264
|
+
if (pkg) req.affectedPackages = [pkg];
|
|
5265
|
+
return req;
|
|
5266
|
+
}
|
|
5267
|
+
function ecosystemFromXraySource(scheme) {
|
|
5268
|
+
if (scheme === "gav") return Ecosystem.Maven;
|
|
5269
|
+
return ecosystemFromPurlType(scheme);
|
|
5270
|
+
}
|
|
5271
|
+
function parseSourceCompID(s) {
|
|
5272
|
+
if (!s) return void 0;
|
|
5273
|
+
const m = /^([a-zA-Z0-9]+):\/\/(.+)$/.exec(s);
|
|
5274
|
+
if (!m) return void 0;
|
|
5275
|
+
const scheme = m[1].toLowerCase();
|
|
5276
|
+
const rest = m[2];
|
|
5277
|
+
const colonIdx = rest.lastIndexOf(":");
|
|
5278
|
+
if (colonIdx > 0) return {
|
|
5279
|
+
scheme,
|
|
5280
|
+
name: rest.slice(0, colonIdx),
|
|
5281
|
+
version: rest.slice(colonIdx + 1)
|
|
5282
|
+
};
|
|
5283
|
+
return {
|
|
5284
|
+
scheme,
|
|
5285
|
+
name: rest
|
|
5286
|
+
};
|
|
5287
|
+
}
|
|
5288
|
+
function buildAffectedPackageFromEntry(entry) {
|
|
5289
|
+
const parsed = parseSourceCompID(entry.source_comp_id ?? entry.source_id);
|
|
5290
|
+
let name = entry.component ?? "";
|
|
5291
|
+
let version;
|
|
5292
|
+
let ecosystem;
|
|
5293
|
+
if (parsed) {
|
|
5294
|
+
if (parsed.name) name = parsed.name;
|
|
5295
|
+
version = parsed.version;
|
|
5296
|
+
ecosystem = ecosystemFromXraySource(parsed.scheme);
|
|
5297
|
+
}
|
|
5298
|
+
const fixed = entry.component_versions?.fixed_versions?.[0];
|
|
5299
|
+
return buildAffectedPackage$1({
|
|
5300
|
+
name,
|
|
5301
|
+
version,
|
|
5302
|
+
ecosystem: ecosystem ?? (name ? Ecosystem.Generic : void 0),
|
|
5303
|
+
fixedInVersion: fixed
|
|
5304
|
+
});
|
|
3593
5305
|
}
|
|
3594
5306
|
/**
|
|
3595
5307
|
* Converts JFrog Xray JSON output to HDF format.
|
|
@@ -3620,6 +5332,7 @@ async function convertJfrogXrayToHdf(input) {
|
|
|
3620
5332
|
}
|
|
3621
5333
|
const requirements = [];
|
|
3622
5334
|
for (const [entryID, entries] of groups) requirements.push(buildRequirement$7(entryID, entries));
|
|
5335
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("jfrog-xray-no-findings", "JFrog Xray scanned the target artifact and reported zero vulnerable components.", /* @__PURE__ */ new Date()));
|
|
3623
5336
|
return buildHdfResults({
|
|
3624
5337
|
generatorName: "jfrog-xray-to-hdf",
|
|
3625
5338
|
converterVersion: "1.0.0",
|
|
@@ -3628,7 +5341,7 @@ async function convertJfrogXrayToHdf(input) {
|
|
|
3628
5341
|
baselines: [createMinimalBaseline("JFrog Xray Scan", requirements, { resultsChecksum })],
|
|
3629
5342
|
components: [{
|
|
3630
5343
|
name: "JFrog Xray Scan",
|
|
3631
|
-
type:
|
|
5344
|
+
type: TargetType.Application
|
|
3632
5345
|
}],
|
|
3633
5346
|
timestamp: /* @__PURE__ */ new Date()
|
|
3634
5347
|
});
|
|
@@ -3687,7 +5400,20 @@ function buildRequirement$6(vuln) {
|
|
|
3687
5400
|
data: vuln.description
|
|
3688
5401
|
}];
|
|
3689
5402
|
const results = [createResult(ResultStatus.Failed, vulnMessage(vuln), { codeDesc: "" })];
|
|
3690
|
-
|
|
5403
|
+
const req = createRequirement(vulnID(vuln), vulnTitle(vuln), descriptions, getImpact$4(vuln), results, { tags });
|
|
5404
|
+
const controlType = deriveControlTypeFromTags(nist);
|
|
5405
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
5406
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
5407
|
+
const cpe23 = (vuln.cpes ?? []).find((c) => /^cpe:2\.3:[aho]:/.test(c));
|
|
5408
|
+
const pkg = buildAffectedPackage$1({
|
|
5409
|
+
name: vuln.package_name,
|
|
5410
|
+
version: vuln.package_version,
|
|
5411
|
+
ecosystem: vuln.package_name && vuln.package_version ? Ecosystem.Generic : void 0,
|
|
5412
|
+
cpe: cpe23,
|
|
5413
|
+
fixedInVersion: vuln.fixed_version
|
|
5414
|
+
});
|
|
5415
|
+
if (pkg) req.affectedPackages = [pkg];
|
|
5416
|
+
return req;
|
|
3691
5417
|
}
|
|
3692
5418
|
/**
|
|
3693
5419
|
* Constructs the baseline title from the image metadata.
|
|
@@ -3732,6 +5458,9 @@ async function convertNeuvectorToHdf(input) {
|
|
|
3732
5458
|
seen.add(id);
|
|
3733
5459
|
requirements.push(buildRequirement$6(vuln));
|
|
3734
5460
|
}
|
|
5461
|
+
const now = /* @__PURE__ */ new Date();
|
|
5462
|
+
const target = targetNameFromReport(scan.report);
|
|
5463
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("neuvector-no-findings", `NeuVector scanned ${target} and reported zero vulnerable components.`, now));
|
|
3735
5464
|
return buildHdfResults({
|
|
3736
5465
|
generatorName: "neuvector-to-hdf",
|
|
3737
5466
|
converterVersion: "1.0.0",
|
|
@@ -3743,13 +5472,13 @@ async function convertNeuvectorToHdf(input) {
|
|
|
3743
5472
|
})],
|
|
3744
5473
|
components: [{
|
|
3745
5474
|
name: targetNameFromReport(scan.report),
|
|
3746
|
-
type:
|
|
5475
|
+
type: TargetType.ContainerImage,
|
|
3747
5476
|
labels: {
|
|
3748
5477
|
image: `${scan.report.registry}/${scan.report.repository}:${scan.report.tag}`,
|
|
3749
5478
|
registry: scan.report.registry
|
|
3750
5479
|
}
|
|
3751
5480
|
}],
|
|
3752
|
-
timestamp:
|
|
5481
|
+
timestamp: now
|
|
3753
5482
|
});
|
|
3754
5483
|
}
|
|
3755
5484
|
//#endregion
|
|
@@ -3826,14 +5555,18 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
|
|
|
3826
5555
|
codeDesc: buildCodeDesc$2(vuln, snippetMap),
|
|
3827
5556
|
startTime: new Date(startTimeStr)
|
|
3828
5557
|
}));
|
|
3829
|
-
|
|
5558
|
+
const req = {
|
|
3830
5559
|
id: desc.classID ?? "unknown",
|
|
3831
5560
|
title,
|
|
3832
5561
|
impact,
|
|
3833
5562
|
tags,
|
|
3834
5563
|
descriptions,
|
|
3835
|
-
results
|
|
5564
|
+
results,
|
|
5565
|
+
verificationMethod: VerificationMethodEnum.Automated
|
|
3836
5566
|
};
|
|
5567
|
+
const controlType = deriveControlTypeFromTags(nistTags);
|
|
5568
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
5569
|
+
return req;
|
|
3837
5570
|
}
|
|
3838
5571
|
/**
|
|
3839
5572
|
* Converts Fortify FVDL XML to HDF format.
|
|
@@ -3860,21 +5593,22 @@ async function convertFortifyToHdf(input) {
|
|
|
3860
5593
|
if (truncatedDescs) console.warn(`WARNING: Input truncated at ${limitedDescs.length} Description items (original: ${descriptions.length})`);
|
|
3861
5594
|
const createdDate = fvdl.CreatedTS?.date ?? "";
|
|
3862
5595
|
const startTimeStr = `${createdDate}T${fvdl.CreatedTS?.time ?? ""}`;
|
|
3863
|
-
const
|
|
5596
|
+
const requirements = limitedDescs.map((desc) => {
|
|
3864
5597
|
return buildRequirement$5(desc, vulnGroups.get(desc.classID ?? "") ?? [], snippetMap, startTimeStr);
|
|
3865
|
-
}), {
|
|
3866
|
-
resultsChecksum,
|
|
3867
|
-
title: "Fortify Static Analyzer Scan",
|
|
3868
|
-
summary: `Fortify Static Analyzer Scan of UUID: ${fvdl.UUID ?? ""}`,
|
|
3869
|
-
version: fvdl.EngineData?.EngineVersion ?? "",
|
|
3870
|
-
status: "loaded"
|
|
3871
5598
|
});
|
|
3872
5599
|
const targetName = fvdl.Build?.SourceBasePath ?? fvdl.Build?.BuildID ?? "Unknown";
|
|
5600
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("fortify-no-findings", `Fortify scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
3873
5601
|
const hdfResult = {
|
|
3874
|
-
baselines: [
|
|
5602
|
+
baselines: [createMinimalBaseline("Fortify Scan", requirements, {
|
|
5603
|
+
resultsChecksum,
|
|
5604
|
+
title: "Fortify Static Analyzer Scan",
|
|
5605
|
+
summary: `Fortify Static Analyzer Scan of UUID: ${fvdl.UUID ?? ""}`,
|
|
5606
|
+
version: fvdl.EngineData?.EngineVersion ?? "",
|
|
5607
|
+
status: "loaded"
|
|
5608
|
+
})],
|
|
3875
5609
|
components: [{
|
|
3876
5610
|
name: targetName,
|
|
3877
|
-
type:
|
|
5611
|
+
type: TargetType.Repository
|
|
3878
5612
|
}],
|
|
3879
5613
|
generator: {
|
|
3880
5614
|
name: "fortify-to-hdf",
|
|
@@ -3985,7 +5719,52 @@ function buildRequirement$4(rec) {
|
|
|
3985
5719
|
data: rec.Description
|
|
3986
5720
|
}];
|
|
3987
5721
|
const results = [createResult(ResultStatus.Failed, message, { codeDesc })];
|
|
3988
|
-
|
|
5722
|
+
const req = createRequirement(id, title, descriptions, getImpact$3(rec.Severity), results, { tags });
|
|
5723
|
+
const controlType = deriveControlTypeFromTags(nist);
|
|
5724
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
5725
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
5726
|
+
if (rec["CVE ID"]) {
|
|
5727
|
+
const pkg = buildAffectedPackageFromRecord(rec);
|
|
5728
|
+
if (pkg) req.affectedPackages = [pkg];
|
|
5729
|
+
}
|
|
5730
|
+
return req;
|
|
5731
|
+
}
|
|
5732
|
+
/**
|
|
5733
|
+
* Distro slugs in Prisma look like `redhat-RHEL7`, `debian-buster`,
|
|
5734
|
+
* `alpine-3.14`, `ubuntu-20.04`. Only the leading vendor segment is
|
|
5735
|
+
* mapped — unknown vendors fall back to `generic` rather than guessing.
|
|
5736
|
+
*/
|
|
5737
|
+
function ecosystemFromDistro(distro) {
|
|
5738
|
+
if (!distro) return Ecosystem.Generic;
|
|
5739
|
+
switch (distro.split("-")[0]?.toLowerCase()) {
|
|
5740
|
+
case "redhat":
|
|
5741
|
+
case "rhel":
|
|
5742
|
+
case "centos":
|
|
5743
|
+
case "rocky":
|
|
5744
|
+
case "alma":
|
|
5745
|
+
case "fedora":
|
|
5746
|
+
case "amazon":
|
|
5747
|
+
case "amazonlinux":
|
|
5748
|
+
case "suse":
|
|
5749
|
+
case "sles":
|
|
5750
|
+
case "opensuse": return Ecosystem.RPM;
|
|
5751
|
+
case "debian":
|
|
5752
|
+
case "ubuntu": return Ecosystem.Deb;
|
|
5753
|
+
default: return Ecosystem.Generic;
|
|
5754
|
+
}
|
|
5755
|
+
}
|
|
5756
|
+
const FIX_VERSION_PATTERN = /fixed in\s+([^\s,;]+)/i;
|
|
5757
|
+
function buildAffectedPackageFromRecord(rec) {
|
|
5758
|
+
const name = rec["Source Package"] || rec.Packages;
|
|
5759
|
+
const version = rec["Package Version"];
|
|
5760
|
+
if (!name || !version) return void 0;
|
|
5761
|
+
const fixMatch = rec["Fix Status"] ? FIX_VERSION_PATTERN.exec(rec["Fix Status"]) : null;
|
|
5762
|
+
return buildAffectedPackage$1({
|
|
5763
|
+
name,
|
|
5764
|
+
version,
|
|
5765
|
+
ecosystem: ecosystemFromDistro(rec.Distro),
|
|
5766
|
+
fixedInVersion: fixMatch ? fixMatch[1] : void 0
|
|
5767
|
+
});
|
|
3989
5768
|
}
|
|
3990
5769
|
/**
|
|
3991
5770
|
* Groups records by hostname, preserving insertion order.
|
|
@@ -4018,20 +5797,25 @@ function buildBaseline(hostname, records, resultsChecksum) {
|
|
|
4018
5797
|
async function convertPrismaToHdf(input) {
|
|
4019
5798
|
if (!input || input.trim().length === 0) throw new Error("prisma: empty input");
|
|
4020
5799
|
validateInputSize(input, "prisma");
|
|
5800
|
+
const headers = (input.split(/\r?\n/, 1)[0] ?? "").split(",").map((h) => h.trim());
|
|
5801
|
+
for (const col of REQUIRED_COLUMNS) if (!headers.includes(col)) throw new Error(`prisma: missing required CSV column "${col}"`);
|
|
4021
5802
|
const records = parseCsv(input);
|
|
4022
|
-
if (records.length === 0) throw new Error("prisma: no data rows in CSV");
|
|
4023
|
-
const firstRecord = records[0];
|
|
4024
|
-
for (const col of REQUIRED_COLUMNS) if (!(col in firstRecord)) throw new Error(`prisma: missing required CSV column "${col}"`);
|
|
4025
5803
|
const resultsChecksum = await inputChecksum(input);
|
|
4026
|
-
const hostGroups = groupByHostname(records);
|
|
4027
5804
|
const baselines = [];
|
|
4028
5805
|
const components = [];
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
5806
|
+
if (records.length === 0) baselines.push(createMinimalBaseline("Prisma Cloud Scan", [buildNoFindingsRequirement("prisma-no-findings", "Prisma Cloud scanned the workload and reported zero vulnerable components.", /* @__PURE__ */ new Date())], {
|
|
5807
|
+
resultsChecksum,
|
|
5808
|
+
title: "Prisma Cloud Scan"
|
|
5809
|
+
}));
|
|
5810
|
+
else {
|
|
5811
|
+
const hostGroups = groupByHostname(records);
|
|
5812
|
+
for (const [hostname, hostRecords] of hostGroups) {
|
|
5813
|
+
baselines.push(buildBaseline(hostname, hostRecords, resultsChecksum));
|
|
5814
|
+
components.push({
|
|
5815
|
+
name: hostname,
|
|
5816
|
+
type: TargetType.Host
|
|
5817
|
+
});
|
|
5818
|
+
}
|
|
4035
5819
|
}
|
|
4036
5820
|
return buildHdfResults({
|
|
4037
5821
|
generatorName: "prisma-to-hdf",
|
|
@@ -4145,7 +5929,7 @@ function buildRequirement$3(vuln, initiated) {
|
|
|
4145
5929
|
startTime
|
|
4146
5930
|
}];
|
|
4147
5931
|
const impact = getImpact$2(vuln.severity ?? "");
|
|
4148
|
-
|
|
5932
|
+
const req = {
|
|
4149
5933
|
id: vuln.LookupId ?? "",
|
|
4150
5934
|
title: vuln.name ?? void 0,
|
|
4151
5935
|
impact,
|
|
@@ -4153,6 +5937,10 @@ function buildRequirement$3(vuln, initiated) {
|
|
|
4153
5937
|
descriptions,
|
|
4154
5938
|
results
|
|
4155
5939
|
};
|
|
5940
|
+
const controlType = deriveControlTypeFromTags(nist);
|
|
5941
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
5942
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
5943
|
+
return req;
|
|
4156
5944
|
}
|
|
4157
5945
|
/**
|
|
4158
5946
|
* Converts Netsparker/Invicti XML scan results to HDF format.
|
|
@@ -4176,20 +5964,25 @@ async function convertNetsparkerToHdf(input) {
|
|
|
4176
5964
|
const { items: limitedVulns, truncated } = limitArray(vulns);
|
|
4177
5965
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
4178
5966
|
if (truncated) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${vulns.length})`);
|
|
4179
|
-
const baseline = createMinimalBaseline("Netsparker Scan", limitedVulns.map((vuln) => buildRequirement$3(vuln, initiated)), {
|
|
4180
|
-
resultsChecksum,
|
|
4181
|
-
title: `${toolName} Enterprise Scan ID: ${target["scan-id"] ?? ""} URL: ${target.url ?? ""}`
|
|
4182
|
-
});
|
|
4183
5967
|
const targetName = target.url ?? "Unknown";
|
|
5968
|
+
const requirements = limitedVulns.map((vuln) => buildRequirement$3(vuln, initiated));
|
|
5969
|
+
if (requirements.length === 0) {
|
|
5970
|
+
const initiatedDate = initiated ? new Date(initiated) : /* @__PURE__ */ new Date();
|
|
5971
|
+
const startTime = isNaN(initiatedDate.getTime()) ? /* @__PURE__ */ new Date() : initiatedDate;
|
|
5972
|
+
requirements.push(buildNoFindingsRequirement("netsparker-no-findings", `${toolName} scanned ${targetName} and reported zero findings.`, startTime));
|
|
5973
|
+
}
|
|
4184
5974
|
return buildHdfResults({
|
|
4185
5975
|
generatorName: "netsparker-to-hdf",
|
|
4186
5976
|
converterVersion: "1.0.0",
|
|
4187
5977
|
toolName,
|
|
4188
5978
|
toolFormat: "XML",
|
|
4189
|
-
baselines: [
|
|
5979
|
+
baselines: [createMinimalBaseline("Netsparker Scan", requirements, {
|
|
5980
|
+
resultsChecksum,
|
|
5981
|
+
title: `${toolName} Enterprise Scan ID: ${target["scan-id"] ?? ""} URL: ${target.url ?? ""}`
|
|
5982
|
+
})],
|
|
4190
5983
|
components: [{
|
|
4191
5984
|
name: targetName,
|
|
4192
|
-
type:
|
|
5985
|
+
type: TargetType.Application
|
|
4193
5986
|
}]
|
|
4194
5987
|
});
|
|
4195
5988
|
}
|
|
@@ -4284,7 +6077,11 @@ function buildRequirement$2(ruleID, finding, startTime) {
|
|
|
4284
6077
|
codeDesc: finding.description,
|
|
4285
6078
|
startTime: startTime ? new Date(startTime) : void 0
|
|
4286
6079
|
});
|
|
4287
|
-
|
|
6080
|
+
const req = createRequirement(ruleID, finding.description, descriptions, getImpact$1(finding.level), [resultObj], { tags });
|
|
6081
|
+
const controlType = deriveControlTypeFromTags(nist);
|
|
6082
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
6083
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
6084
|
+
return req;
|
|
4288
6085
|
}
|
|
4289
6086
|
/**
|
|
4290
6087
|
* Converts ScoutSuite output to HDF format.
|
|
@@ -4300,12 +6097,14 @@ async function convertScoutsuiteToHdf(input) {
|
|
|
4300
6097
|
const resultsChecksum = await inputChecksum(jsonStr);
|
|
4301
6098
|
const report = parseJSON(jsonStr);
|
|
4302
6099
|
if (!report || typeof report !== "object") throw new Error("scoutsuite: invalid JSON");
|
|
4303
|
-
const
|
|
6100
|
+
const requirements = limitArrayWithWarning(collapseFindings(report), "finding").map(([ruleID, finding]) => buildRequirement$2(ruleID, finding, report.last_run.time));
|
|
6101
|
+
const targetName = `${report.last_run.ruleset_name} ruleset:${report.provider_name}:${report.account_id}`;
|
|
6102
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("scoutsuite-no-findings", `ScoutSuite scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
6103
|
+
const baseline = createMinimalBaseline("ScoutSuite Scan", requirements, {
|
|
4304
6104
|
resultsChecksum,
|
|
4305
6105
|
title: `Scout Suite Report using ${report.last_run.ruleset_name} ruleset on ${report.provider_name} with account ${report.account_id}`,
|
|
4306
6106
|
summary: report.last_run.ruleset_about
|
|
4307
6107
|
});
|
|
4308
|
-
const targetName = `${report.last_run.ruleset_name} ruleset:${report.provider_name}:${report.account_id}`;
|
|
4309
6108
|
return buildHdfResults({
|
|
4310
6109
|
generatorName: "scoutsuite-to-hdf",
|
|
4311
6110
|
converterVersion: "1.0.0",
|
|
@@ -4315,7 +6114,7 @@ async function convertScoutsuiteToHdf(input) {
|
|
|
4315
6114
|
baselines: [baseline],
|
|
4316
6115
|
components: [{
|
|
4317
6116
|
name: targetName,
|
|
4318
|
-
type:
|
|
6117
|
+
type: TargetType.CloudAccount,
|
|
4319
6118
|
labels: {
|
|
4320
6119
|
account: report.account_id,
|
|
4321
6120
|
provider: report.provider_code ?? report.provider_name
|
|
@@ -4432,7 +6231,11 @@ function buildRequirementFromResult(result, filename) {
|
|
|
4432
6231
|
codeDesc: `No sections reported by ${scannerName}`,
|
|
4433
6232
|
startTime
|
|
4434
6233
|
})];
|
|
4435
|
-
|
|
6234
|
+
const req = createRequirement(result.sha256, filename, descriptions, scoreToImpact(score), results, { tags });
|
|
6235
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
6236
|
+
const controlType = deriveControlTypeFromTags([...nist]);
|
|
6237
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
6238
|
+
return req;
|
|
4436
6239
|
}
|
|
4437
6240
|
/**
|
|
4438
6241
|
* Builds an HDF baseline for a single scanner's results.
|
|
@@ -4468,6 +6271,10 @@ async function convertConveyorToHdf(input) {
|
|
|
4468
6271
|
const desc = data.api_response.params["description"];
|
|
4469
6272
|
if (typeof desc === "string" && desc.length > 0) targetName = desc;
|
|
4470
6273
|
}
|
|
6274
|
+
if (baselines.length === 0) baselines.push(createMinimalBaseline("Conveyor Scan", [buildNoFindingsRequirement("conveyor-no-findings", `Conveyor scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date())], {
|
|
6275
|
+
resultsChecksum,
|
|
6276
|
+
title: "Conveyor Scan (no findings)"
|
|
6277
|
+
}));
|
|
4471
6278
|
return buildHdfResults({
|
|
4472
6279
|
generatorName: "conveyor-to-hdf",
|
|
4473
6280
|
converterVersion: "1.0.0",
|
|
@@ -4476,7 +6283,7 @@ async function convertConveyorToHdf(input) {
|
|
|
4476
6283
|
baselines,
|
|
4477
6284
|
components: [{
|
|
4478
6285
|
name: targetName,
|
|
4479
|
-
type:
|
|
6286
|
+
type: TargetType.Application
|
|
4480
6287
|
}],
|
|
4481
6288
|
timestamp: /* @__PURE__ */ new Date()
|
|
4482
6289
|
});
|
|
@@ -4660,7 +6467,11 @@ function buildCWERequirement(cat, impact, firstBuildDate) {
|
|
|
4660
6467
|
startTime
|
|
4661
6468
|
}));
|
|
4662
6469
|
});
|
|
4663
|
-
|
|
6470
|
+
const req = createRequirement(attr(cat, "categoryid"), attr(cat, "categoryname"), descriptions, impact, results, { tags });
|
|
6471
|
+
const controlType = deriveControlTypeFromTags(nist);
|
|
6472
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
6473
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
6474
|
+
return req;
|
|
4664
6475
|
}
|
|
4665
6476
|
/** Build CVE-based requirements from SCA components. */
|
|
4666
6477
|
function buildCVERequirements(sca, firstBuildDate) {
|
|
@@ -4712,10 +6523,14 @@ function buildCVERequirement(vuln, components, firstBuildDate) {
|
|
|
4712
6523
|
}));
|
|
4713
6524
|
const cveSummary = attr(vuln, "cve_summary");
|
|
4714
6525
|
const cveId = attr(vuln, "cve_id");
|
|
4715
|
-
|
|
6526
|
+
const req = createRequirement(cveId, cveId, [{
|
|
4716
6527
|
label: "default",
|
|
4717
6528
|
data: cveSummary || ""
|
|
4718
6529
|
}], impact, results, { tags });
|
|
6530
|
+
const controlType = deriveControlTypeFromTags(nist);
|
|
6531
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
6532
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
6533
|
+
return req;
|
|
4719
6534
|
}
|
|
4720
6535
|
/**
|
|
4721
6536
|
* Convert Veracode DetailedReport XML to HDF JSON string.
|
|
@@ -4735,6 +6550,8 @@ async function convertVeracodeToHdf(input) {
|
|
|
4735
6550
|
const cweRequirements = buildCWERequirements(ensureArray(report.severity), firstBuildDate);
|
|
4736
6551
|
const cveRequirements = buildCVERequirements(report.software_composition_analysis, firstBuildDate);
|
|
4737
6552
|
const allRequirements = [...cweRequirements, ...cveRequirements];
|
|
6553
|
+
const targetName = attr(report, "app_name") || "Veracode Application";
|
|
6554
|
+
if (allRequirements.length === 0) allRequirements.push(buildNoFindingsRequirement("veracode-no-findings", `Veracode scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
4738
6555
|
let title;
|
|
4739
6556
|
const staticAnalysis = report["static-analysis"];
|
|
4740
6557
|
if (staticAnalysis) {
|
|
@@ -4750,7 +6567,6 @@ async function convertVeracodeToHdf(input) {
|
|
|
4750
6567
|
version: attr(report, "policy_version"),
|
|
4751
6568
|
summary: attr(report, "policy_name")
|
|
4752
6569
|
});
|
|
4753
|
-
const targetName = attr(report, "app_name") || "Veracode Application";
|
|
4754
6570
|
const timestamp = parseVeracodeTimestamp(firstBuildDate);
|
|
4755
6571
|
return buildHdfResults({
|
|
4756
6572
|
generatorName: "veracode-to-hdf",
|
|
@@ -4760,7 +6576,7 @@ async function convertVeracodeToHdf(input) {
|
|
|
4760
6576
|
baselines: [baseline],
|
|
4761
6577
|
components: [{
|
|
4762
6578
|
name: targetName,
|
|
4763
|
-
type:
|
|
6579
|
+
type: TargetType.Application
|
|
4764
6580
|
}],
|
|
4765
6581
|
timestamp
|
|
4766
6582
|
});
|
|
@@ -4811,7 +6627,8 @@ function buildRequirement$1(cs, profiles, createdDateTime) {
|
|
|
4811
6627
|
const title = getTitle(profiles, cs);
|
|
4812
6628
|
const impact = getImpact(profiles, cs);
|
|
4813
6629
|
const status = getStatus(profiles, cs);
|
|
4814
|
-
const
|
|
6630
|
+
const nist = [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS];
|
|
6631
|
+
const tags = buildNistCciTags(nist, []);
|
|
4815
6632
|
const descriptions = [{
|
|
4816
6633
|
label: "default",
|
|
4817
6634
|
data: stripHTML(cs.description)
|
|
@@ -4831,10 +6648,14 @@ function buildRequirement$1(cs, profiles, createdDateTime) {
|
|
|
4831
6648
|
}
|
|
4832
6649
|
const codeDesc = cs.implementationStatus || "No implementation status provided";
|
|
4833
6650
|
const startTime = createdDateTime ? new Date(createdDateTime) : void 0;
|
|
4834
|
-
|
|
6651
|
+
const req = createRequirement(id, title, descriptions, impact, [createResult(status, void 0, {
|
|
4835
6652
|
codeDesc,
|
|
4836
6653
|
...startTime ? { startTime } : {}
|
|
4837
6654
|
})], { tags });
|
|
6655
|
+
const controlType = deriveControlTypeFromTags(nist);
|
|
6656
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
6657
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
6658
|
+
return req;
|
|
4838
6659
|
}
|
|
4839
6660
|
/**
|
|
4840
6661
|
* Converts Microsoft Secure Score combined JSON to HDF format.
|
|
@@ -4873,7 +6694,7 @@ async function convertMsftSecureScoreToHdf(input) {
|
|
|
4873
6694
|
}),
|
|
4874
6695
|
components: [{
|
|
4875
6696
|
name: `Azure Tenant: ${tenantId}`,
|
|
4876
|
-
type:
|
|
6697
|
+
type: TargetType.CloudAccount,
|
|
4877
6698
|
labels: {
|
|
4878
6699
|
account: tenantId,
|
|
4879
6700
|
provider: "azure"
|
|
@@ -4896,6 +6717,7 @@ async function convertMsftDefenderDevopsToHdf(input) {
|
|
|
4896
6717
|
const { components, runEnrichments } = extractEnrichments(raw);
|
|
4897
6718
|
const hdfJson = await convertSarifToHdf(input);
|
|
4898
6719
|
const result = JSON.parse(hdfJson);
|
|
6720
|
+
synthesizeNoFindingsPlaceholders(result);
|
|
4899
6721
|
applyEnrichments(result, components, runEnrichments);
|
|
4900
6722
|
if (result.generator) result.generator.name = "msft-defender-devops-to-hdf";
|
|
4901
6723
|
if (result.tool) result.tool.name = "Microsoft Defender for DevOps";
|
|
@@ -4910,7 +6732,7 @@ function extractEnrichments(raw) {
|
|
|
4910
6732
|
seenRepos.add(vcp.repositoryUri);
|
|
4911
6733
|
const target = {
|
|
4912
6734
|
name: repoNameFromURI(vcp.repositoryUri),
|
|
4913
|
-
type:
|
|
6735
|
+
type: TargetType.Repository,
|
|
4914
6736
|
url: vcp.repositoryUri,
|
|
4915
6737
|
labels: {}
|
|
4916
6738
|
};
|
|
@@ -4960,6 +6782,14 @@ function applyEnrichments(result, components, runEnrichments) {
|
|
|
4960
6782
|
}
|
|
4961
6783
|
}
|
|
4962
6784
|
}
|
|
6785
|
+
function synthesizeNoFindingsPlaceholders(result) {
|
|
6786
|
+
const startTime = result.timestamp ?? /* @__PURE__ */ new Date();
|
|
6787
|
+
for (const baseline of result.baselines ?? []) {
|
|
6788
|
+
if (baseline.requirements && baseline.requirements.length > 0) continue;
|
|
6789
|
+
const tool = baseline.name;
|
|
6790
|
+
baseline.requirements = [buildNoFindingsRequirement(`${tool}-no-findings`, `Microsoft Defender for DevOps scanner "${tool}" ran and reported zero findings.`, startTime)];
|
|
6791
|
+
}
|
|
6792
|
+
}
|
|
4963
6793
|
function repoNameFromURI(uri) {
|
|
4964
6794
|
const parts = uri.split("/");
|
|
4965
6795
|
return parts[parts.length - 1] || uri;
|
|
@@ -5021,7 +6851,9 @@ function buildRequirement(assessmentID, assessments) {
|
|
|
5021
6851
|
data: meta.remediationDescription
|
|
5022
6852
|
});
|
|
5023
6853
|
const results = assessments.map(buildResultFromAssessment);
|
|
5024
|
-
|
|
6854
|
+
const req = createRequirement(assessmentID, rep.properties.displayName, descriptions, impact, results, { tags });
|
|
6855
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
6856
|
+
return req;
|
|
5025
6857
|
}
|
|
5026
6858
|
/**
|
|
5027
6859
|
* Converts a single assessment into an HDF RequirementResult.
|
|
@@ -5054,21 +6886,23 @@ async function convertMsftDefenderCloudToHdf(input) {
|
|
|
5054
6886
|
}
|
|
5055
6887
|
const requirements = [];
|
|
5056
6888
|
for (const [assessmentID, assessments] of groups) requirements.push(buildRequirement(assessmentID, assessments));
|
|
6889
|
+
const subscriptionID = limitedAssessments.length > 0 ? extractSubscriptionID(limitedAssessments[0].id) : "";
|
|
6890
|
+
if (requirements.length === 0) {
|
|
6891
|
+
const targetName = subscriptionID || "Unknown";
|
|
6892
|
+
requirements.push(buildNoFindingsRequirement("msft-defender-cloud-no-findings", `Microsoft Defender for Cloud scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
6893
|
+
}
|
|
5057
6894
|
const baseline = createMinimalBaseline("Microsoft Defender for Cloud Assessments", requirements, { resultsChecksum });
|
|
5058
6895
|
const components = [];
|
|
5059
|
-
if (
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
}
|
|
5070
|
-
});
|
|
5071
|
-
}
|
|
6896
|
+
if (subscriptionID) components.push({
|
|
6897
|
+
name: `Azure Subscription ${subscriptionID}`,
|
|
6898
|
+
type: TargetType.CloudAccount,
|
|
6899
|
+
accountId: subscriptionID,
|
|
6900
|
+
provider: "azure",
|
|
6901
|
+
labels: {
|
|
6902
|
+
account: subscriptionID,
|
|
6903
|
+
provider: "azure"
|
|
6904
|
+
}
|
|
6905
|
+
});
|
|
5072
6906
|
return buildHdfResults({
|
|
5073
6907
|
generatorName: "msft-defender-cloud-to-hdf",
|
|
5074
6908
|
converterVersion: "1.0.0",
|
|
@@ -5143,7 +6977,7 @@ function extractDeviceTarget(alert) {
|
|
|
5143
6977
|
for (const ev of alert.evidence) if ((ev["@odata.type"] ?? "").includes("deviceEvidence") && ev.deviceDnsName) {
|
|
5144
6978
|
const target = {
|
|
5145
6979
|
name: ev.deviceDnsName,
|
|
5146
|
-
type:
|
|
6980
|
+
type: TargetType.Host,
|
|
5147
6981
|
labels: { provider: "azure" }
|
|
5148
6982
|
};
|
|
5149
6983
|
if (ev.deviceDnsName) target.fqdn = ev.deviceDnsName;
|
|
@@ -5153,7 +6987,7 @@ function extractDeviceTarget(alert) {
|
|
|
5153
6987
|
}
|
|
5154
6988
|
return {
|
|
5155
6989
|
name: alert.tenantId ?? "unknown",
|
|
5156
|
-
type:
|
|
6990
|
+
type: TargetType.CloudAccount,
|
|
5157
6991
|
accountId: alert.tenantId,
|
|
5158
6992
|
labels: {
|
|
5159
6993
|
account: alert.tenantId ?? "",
|
|
@@ -5189,7 +7023,12 @@ function alertToRequirement(alert) {
|
|
|
5189
7023
|
data: alert.recommendedActions
|
|
5190
7024
|
});
|
|
5191
7025
|
const tags = buildTags$1(alert);
|
|
5192
|
-
|
|
7026
|
+
const req = createRequirement(alert.id, alert.title, descriptions, impact, results, { tags });
|
|
7027
|
+
const nistTags = tags.nist;
|
|
7028
|
+
const controlType = deriveControlTypeFromTags(nistTags);
|
|
7029
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
7030
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
7031
|
+
return req;
|
|
5193
7032
|
}
|
|
5194
7033
|
/**
|
|
5195
7034
|
* Converts Microsoft Defender for Endpoint alerts (Microsoft Graph Security API v2 format) to HDF.
|
|
@@ -5217,6 +7056,7 @@ async function convertMsftDefenderEndpointToHdf(input) {
|
|
|
5217
7056
|
components.push(target);
|
|
5218
7057
|
}
|
|
5219
7058
|
}
|
|
7059
|
+
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("msft-defender-endpoint-no-findings", "Microsoft Defender for Endpoint scanned the tenant and reported zero findings.", /* @__PURE__ */ new Date()));
|
|
5220
7060
|
return buildHdfResults({
|
|
5221
7061
|
generatorName: "msft-defender-endpoint-to-hdf",
|
|
5222
7062
|
converterVersion: "1.0.0",
|
|
@@ -5357,8 +7197,7 @@ function buildTestResultObj(hdfData, baseline) {
|
|
|
5357
7197
|
[`${ATTR}idref`]: ruleIdRef,
|
|
5358
7198
|
result: wrap(hdfStatusToXccdf(result.status))
|
|
5359
7199
|
};
|
|
5360
|
-
|
|
5361
|
-
rr[`${ATTR}time`] = startTime;
|
|
7200
|
+
rr[`${ATTR}time`] = typeof result.startTime === "string" ? result.startTime : result.startTime.toISOString();
|
|
5362
7201
|
if (result.message) rr.message = wrap(result.message);
|
|
5363
7202
|
if (result.codeDesc) rr.check = {
|
|
5364
7203
|
[`${ATTR}system`]: "http://oval.mitre.org/XMLSchema/oval-definitions-5",
|
|
@@ -5788,7 +7627,7 @@ async function convertHdfToOscalPoam(input) {
|
|
|
5788
7627
|
return JSON.stringify(doc, null, 2);
|
|
5789
7628
|
}
|
|
5790
7629
|
/**
|
|
5791
|
-
* Converts parsed
|
|
7630
|
+
* Converts parsed HDFAmendments to an OSCAL PlanOfActionAndMilestones.
|
|
5792
7631
|
*/
|
|
5793
7632
|
function amendmentsToPOAM(amendments) {
|
|
5794
7633
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -5964,6 +7803,9 @@ async function convertIonchannelToHdf(input) {
|
|
|
5964
7803
|
startTime: /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
|
|
5965
7804
|
})], { tags });
|
|
5966
7805
|
req.code = code;
|
|
7806
|
+
const controlType = deriveControlTypeFromTags(DEFAULT_COMPONENT_MANAGEMENT_NIST_TAGS);
|
|
7807
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
7808
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
5967
7809
|
return req;
|
|
5968
7810
|
});
|
|
5969
7811
|
const resultsChecksum = await inputChecksum(input);
|
|
@@ -5985,6 +7827,1150 @@ async function convertIonchannelToHdf(input) {
|
|
|
5985
7827
|
});
|
|
5986
7828
|
}
|
|
5987
7829
|
//#endregion
|
|
7830
|
+
//#region shared/typescript/vex/mapping.ts
|
|
7831
|
+
/** Canonical VEX status across OpenVEX / CSAF / CycloneDX. */
|
|
7832
|
+
const VexStatus = {
|
|
7833
|
+
NotAffected: "not_affected",
|
|
7834
|
+
Affected: "affected",
|
|
7835
|
+
Fixed: "fixed",
|
|
7836
|
+
UnderInvestigation: "under_investigation"
|
|
7837
|
+
};
|
|
7838
|
+
/**
|
|
7839
|
+
* Map an ecosystem-specific status string to the canonical VexStatus.
|
|
7840
|
+
* Returns undefined for values without a clean mapping — caller should warn
|
|
7841
|
+
* and skip, not guess.
|
|
7842
|
+
*/
|
|
7843
|
+
function normalizeStatus(raw) {
|
|
7844
|
+
switch (raw.trim().toLowerCase()) {
|
|
7845
|
+
case "not_affected":
|
|
7846
|
+
case "known_not_affected":
|
|
7847
|
+
case "false_positive": return VexStatus.NotAffected;
|
|
7848
|
+
case "affected":
|
|
7849
|
+
case "known_affected":
|
|
7850
|
+
case "exploitable": return VexStatus.Affected;
|
|
7851
|
+
case "fixed":
|
|
7852
|
+
case "first_fixed":
|
|
7853
|
+
case "resolved":
|
|
7854
|
+
case "resolved_with_pedigree": return VexStatus.Fixed;
|
|
7855
|
+
case "under_investigation":
|
|
7856
|
+
case "in_triage": return VexStatus.UnderInvestigation;
|
|
7857
|
+
default: return;
|
|
7858
|
+
}
|
|
7859
|
+
}
|
|
7860
|
+
/**
|
|
7861
|
+
* Map an ecosystem-specific justification string to the canonical HDF
|
|
7862
|
+
* Justification enum. Returns undefined for unknown values; callers SHOULD
|
|
7863
|
+
* log unknown values rather than silently dropping (the schema spec wants
|
|
7864
|
+
* pass-through, but practically we expect the enum to be extended when a
|
|
7865
|
+
* new vocabulary is integrated rather than carrying raw labels
|
|
7866
|
+
* indefinitely).
|
|
7867
|
+
*
|
|
7868
|
+
* The HDF Justification enum (v3.2.x) covers:
|
|
7869
|
+
* - the original OpenVEX / CSAF VEX five values
|
|
7870
|
+
* - CycloneDX-specific reachability values (requires_*, protected_*)
|
|
7871
|
+
* that describe why a vulnerable code path is unreachable in the
|
|
7872
|
+
* deployed configuration.
|
|
7873
|
+
*/
|
|
7874
|
+
function normalizeJustification(raw) {
|
|
7875
|
+
switch (raw.trim().toLowerCase()) {
|
|
7876
|
+
case "component_not_present":
|
|
7877
|
+
case "code_not_present": return Justification.ComponentNotPresent;
|
|
7878
|
+
case "vulnerable_code_not_present": return Justification.VulnerableCodeNotPresent;
|
|
7879
|
+
case "vulnerable_code_not_in_execute_path":
|
|
7880
|
+
case "code_not_reachable": return Justification.VulnerableCodeNotInExecutePath;
|
|
7881
|
+
case "vulnerable_code_cannot_be_controlled_by_adversary": return Justification.VulnerableCodeCannotBeControlledByAdversary;
|
|
7882
|
+
case "inline_mitigations_already_exist":
|
|
7883
|
+
case "protected_by_mitigating_control": return Justification.InlineMitigationsAlreadyExist;
|
|
7884
|
+
case "requires_configuration": return Justification.RequiresConfiguration;
|
|
7885
|
+
case "requires_dependency": return Justification.RequiresDependency;
|
|
7886
|
+
case "requires_environment": return Justification.RequiresEnvironment;
|
|
7887
|
+
case "protected_by_compiler": return Justification.ProtectedByCompiler;
|
|
7888
|
+
case "protected_at_runtime": return Justification.ProtectedAtRuntime;
|
|
7889
|
+
case "protected_at_perimeter": return Justification.ProtectedAtPerimeter;
|
|
7890
|
+
default: return;
|
|
7891
|
+
}
|
|
7892
|
+
}
|
|
7893
|
+
/**
|
|
7894
|
+
* Render an HDF Justification value as the CycloneDX-native vocabulary.
|
|
7895
|
+
* CycloneDX uses short-form names (code_not_present, code_not_reachable,
|
|
7896
|
+
* protected_by_mitigating_control) for the three justifications shared
|
|
7897
|
+
* with OpenVEX/CSAF, and shares the six CycloneDX-specific reachability
|
|
7898
|
+
* values verbatim.
|
|
7899
|
+
*
|
|
7900
|
+
* Returns undefined when the HDF value has no equivalent in CycloneDX's
|
|
7901
|
+
* enum (vulnerable_code_not_present and
|
|
7902
|
+
* vulnerable_code_cannot_be_controlled_by_adversary). Callers should
|
|
7903
|
+
* omit the justification field in that case.
|
|
7904
|
+
*/
|
|
7905
|
+
function justificationForCycloneDX(j) {
|
|
7906
|
+
switch (j) {
|
|
7907
|
+
case Justification.ComponentNotPresent: return "code_not_present";
|
|
7908
|
+
case Justification.VulnerableCodeNotInExecutePath: return "code_not_reachable";
|
|
7909
|
+
case Justification.InlineMitigationsAlreadyExist: return "protected_by_mitigating_control";
|
|
7910
|
+
case Justification.RequiresConfiguration:
|
|
7911
|
+
case Justification.RequiresDependency:
|
|
7912
|
+
case Justification.RequiresEnvironment:
|
|
7913
|
+
case Justification.ProtectedByCompiler:
|
|
7914
|
+
case Justification.ProtectedAtRuntime:
|
|
7915
|
+
case Justification.ProtectedAtPerimeter: return String(j);
|
|
7916
|
+
default: return;
|
|
7917
|
+
}
|
|
7918
|
+
}
|
|
7919
|
+
/**
|
|
7920
|
+
* Return the amendment shape an importer should produce for a canonical VEX
|
|
7921
|
+
* status. Returns undefined for "affected" and "under_investigation" — those
|
|
7922
|
+
* are informational; the consumer creates an amendment later if they act.
|
|
7923
|
+
*/
|
|
7924
|
+
function importTargetFor(status) {
|
|
7925
|
+
switch (status) {
|
|
7926
|
+
case VexStatus.NotAffected: return {
|
|
7927
|
+
overrideType: OverrideType.FalsePositive,
|
|
7928
|
+
status: ResultStatus.Passed,
|
|
7929
|
+
setJustification: true,
|
|
7930
|
+
poamActionTemplate: ""
|
|
7931
|
+
};
|
|
7932
|
+
case VexStatus.Fixed: return {
|
|
7933
|
+
overrideType: OverrideType.Poam,
|
|
7934
|
+
status: ResultStatus.Failed,
|
|
7935
|
+
setJustification: false,
|
|
7936
|
+
poamActionTemplate: "vendor reports fix; apply and re-scan to verify"
|
|
7937
|
+
};
|
|
7938
|
+
case VexStatus.Affected:
|
|
7939
|
+
case VexStatus.UnderInvestigation: return;
|
|
7940
|
+
/* c8 ignore next 2 — every VexStatus has a case above */
|
|
7941
|
+
default: return;
|
|
7942
|
+
}
|
|
7943
|
+
}
|
|
7944
|
+
/**
|
|
7945
|
+
* Return the canonical VEX status an exporter should emit for an HDF
|
|
7946
|
+
* override. Returns undefined when no VEX statement should be emitted —
|
|
7947
|
+
* the consumer has not acted, and VEX requires a deliberate statement.
|
|
7948
|
+
*
|
|
7949
|
+
* `allMilestonesCompleted` and `closureChained` are consulted only for
|
|
7950
|
+
* POA&M overrides. Closure is signalled by BOTH flags being true; either
|
|
7951
|
+
* alone is insufficient.
|
|
7952
|
+
*/
|
|
7953
|
+
function exportStatusFor(override, allMilestonesCompleted, closureChained) {
|
|
7954
|
+
if (!override) return;
|
|
7955
|
+
if (override.justification) return VexStatus.NotAffected;
|
|
7956
|
+
switch (override.type) {
|
|
7957
|
+
case OverrideType.FalsePositive:
|
|
7958
|
+
case OverrideType.Attestation:
|
|
7959
|
+
case OverrideType.Inherited: return VexStatus.NotAffected;
|
|
7960
|
+
case OverrideType.Waiver:
|
|
7961
|
+
case OverrideType.RiskAdjustment:
|
|
7962
|
+
case OverrideType.OperationalRequirement: return VexStatus.Affected;
|
|
7963
|
+
case OverrideType.Poam: return allMilestonesCompleted && closureChained ? VexStatus.Fixed : VexStatus.Affected;
|
|
7964
|
+
/* c8 ignore next 2 — every OverrideType has a case above */
|
|
7965
|
+
default: return;
|
|
7966
|
+
}
|
|
7967
|
+
}
|
|
7968
|
+
const ECOSYSTEM_FROM_PURL_TYPE = {
|
|
7969
|
+
npm: Ecosystem.Npm,
|
|
7970
|
+
pypi: Ecosystem.Pypi,
|
|
7971
|
+
rpm: Ecosystem.RPM,
|
|
7972
|
+
deb: Ecosystem.Deb,
|
|
7973
|
+
maven: Ecosystem.Maven,
|
|
7974
|
+
gem: Ecosystem.Gem,
|
|
7975
|
+
nuget: Ecosystem.Nuget,
|
|
7976
|
+
golang: Ecosystem.Go,
|
|
7977
|
+
go: Ecosystem.Go,
|
|
7978
|
+
cargo: Ecosystem.Cargo
|
|
7979
|
+
};
|
|
7980
|
+
/**
|
|
7981
|
+
* Build an AffectedPackage from a single product identifier string emitted
|
|
7982
|
+
* by a VEX format. Recognizes PURLs and CPE 2.3 strings; returns undefined
|
|
7983
|
+
* for opaque identifiers (importer should drop the entry — schema additions
|
|
7984
|
+
* forbid fabricating name+version).
|
|
7985
|
+
*/
|
|
7986
|
+
function affectedPackageFromIdentifier(identifier) {
|
|
7987
|
+
if (!identifier) return void 0;
|
|
7988
|
+
if (identifier.startsWith("pkg:")) {
|
|
7989
|
+
const parsed = parsePurl(identifier);
|
|
7990
|
+
if (parsed) {
|
|
7991
|
+
const pkg = { purl: identifier };
|
|
7992
|
+
/* c8 ignore next 2 — parsePurl populates name/version when the
|
|
7993
|
+
identifier was a well-formed purl; the absent branches require a
|
|
7994
|
+
malformed-but-prefixed input that loses these fields without
|
|
7995
|
+
triggering the outer null return. Defensive. */
|
|
7996
|
+
if (parsed.name) pkg.name = parsed.name;
|
|
7997
|
+
if (parsed.version) pkg.version = parsed.version;
|
|
7998
|
+
pkg.ecosystem = ECOSYSTEM_FROM_PURL_TYPE[parsed.type] ?? Ecosystem.Generic;
|
|
7999
|
+
return pkg;
|
|
8000
|
+
}
|
|
8001
|
+
return { purl: identifier };
|
|
8002
|
+
}
|
|
8003
|
+
if (identifier.startsWith("cpe:2.3:")) return { cpe: identifier };
|
|
8004
|
+
}
|
|
8005
|
+
/**
|
|
8006
|
+
* Build a unique list of AffectedPackage entries from a sequence of product
|
|
8007
|
+
* identifier strings. Empty / unresolvable identifiers are dropped; duplicate
|
|
8008
|
+
* purls/cpes/names are collapsed.
|
|
8009
|
+
*/
|
|
8010
|
+
function affectedPackagesFromIdentifiers(identifiers) {
|
|
8011
|
+
const out = [];
|
|
8012
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8013
|
+
for (const id of identifiers) {
|
|
8014
|
+
const pkg = affectedPackageFromIdentifier(id);
|
|
8015
|
+
if (!pkg) continue;
|
|
8016
|
+
/* c8 ignore next — affectedPackageFromIdentifier always emits either
|
|
8017
|
+
purl or cpe, so the name fallback branch isn't reachable through
|
|
8018
|
+
this caller. Kept for safety if a hand-built AffectedPackage flows
|
|
8019
|
+
through a future caller. */
|
|
8020
|
+
const key = pkg.purl ?? pkg.cpe ?? `${pkg.name ?? ""}@${pkg.version ?? ""}`;
|
|
8021
|
+
if (seen.has(key)) continue;
|
|
8022
|
+
seen.add(key);
|
|
8023
|
+
out.push(pkg);
|
|
8024
|
+
}
|
|
8025
|
+
return out;
|
|
8026
|
+
}
|
|
8027
|
+
/**
|
|
8028
|
+
* Render an AffectedPackage as a single identifier string suitable for
|
|
8029
|
+
* round-tripping into a VEX format. Prefers purl > cpe > name@version.
|
|
8030
|
+
* Returns undefined when nothing identifying is set.
|
|
8031
|
+
*/
|
|
8032
|
+
function affectedPackageToIdentifier(pkg) {
|
|
8033
|
+
if (pkg.purl) return pkg.purl;
|
|
8034
|
+
if (pkg.cpe) return pkg.cpe;
|
|
8035
|
+
if (pkg.name && pkg.version) return `${pkg.name}@${pkg.version}`;
|
|
8036
|
+
if (pkg.name) return pkg.name;
|
|
8037
|
+
}
|
|
8038
|
+
/**
|
|
8039
|
+
* Build an HDF Evidence entry pointing at the upstream VEX document. Used by
|
|
8040
|
+
* importers to preserve provenance even though we lose the structured
|
|
8041
|
+
* statement_id. Returns undefined when sourceURI is empty — don't fabricate.
|
|
8042
|
+
*/
|
|
8043
|
+
function supplierEvidence(sourceURI, description) {
|
|
8044
|
+
if (!sourceURI.trim()) return;
|
|
8045
|
+
return {
|
|
8046
|
+
type: EvidenceType.URL,
|
|
8047
|
+
data: sourceURI,
|
|
8048
|
+
description: description?.trim() ? description : "Upstream VEX statement"
|
|
8049
|
+
};
|
|
8050
|
+
}
|
|
8051
|
+
//#endregion
|
|
8052
|
+
//#region converters/openvex-to-hdf/typescript/converter.ts
|
|
8053
|
+
/**
|
|
8054
|
+
* OpenVEX to HDF Amendments converter.
|
|
8055
|
+
*
|
|
8056
|
+
* VEX statements are consumer-attached context for CVE findings. The act of
|
|
8057
|
+
* attaching IS the amendment, so this converter emits HDF Amendments rather
|
|
8058
|
+
* than HDF Results.
|
|
8059
|
+
*
|
|
8060
|
+
* VEX 'fixed' is an abstract supplier claim; the assessed system has not
|
|
8061
|
+
* been verified to carry the fix. Imports therefore become open POA&M
|
|
8062
|
+
* overrides (status pinned to failed pending re-scan), not status flips.
|
|
8063
|
+
*
|
|
8064
|
+
* Spec: https://github.com/openvex/spec
|
|
8065
|
+
*/
|
|
8066
|
+
/** One year in milliseconds. VEX statements are re-evaluated as new info
|
|
8067
|
+
* arrives; one year is a defensive default consistent with the
|
|
8068
|
+
* no-permanent-amendment rule on Standalone_Override. */
|
|
8069
|
+
const DEFAULT_EXPIRY_HORIZON_MS$2 = 365 * 24 * 60 * 60 * 1e3;
|
|
8070
|
+
async function convertOpenVexToHdf(input, converterVersion) {
|
|
8071
|
+
validateInputSize(input, "openvex-to-hdf");
|
|
8072
|
+
const doc = parseJSON(input);
|
|
8073
|
+
const docTime = (doc.timestamp ? parseTimestamp(doc.timestamp) : null) ?? /* @__PURE__ */ new Date();
|
|
8074
|
+
const overrides = [];
|
|
8075
|
+
for (const stmt of doc.statements ?? []) {
|
|
8076
|
+
const override = statementToOverride(stmt, doc, docTime);
|
|
8077
|
+
if (override) overrides.push(override);
|
|
8078
|
+
}
|
|
8079
|
+
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");
|
|
8080
|
+
return {
|
|
8081
|
+
name: doc.author ? `OpenVEX statements from ${doc.author}` : "OpenVEX statements",
|
|
8082
|
+
description: `Imported VEX statements from ${truncateId(doc["@id"] ?? "")}`,
|
|
8083
|
+
overrides,
|
|
8084
|
+
appliedBy: identityFor$1(doc.author, doc.role),
|
|
8085
|
+
generator: {
|
|
8086
|
+
name: "openvex-to-hdf",
|
|
8087
|
+
version: converterVersion
|
|
8088
|
+
},
|
|
8089
|
+
integrity: await inputIntegrity(input)
|
|
8090
|
+
};
|
|
8091
|
+
}
|
|
8092
|
+
function statementToOverride(stmt, doc, docTime) {
|
|
8093
|
+
const canonical = normalizeStatus(stmt.status ?? "");
|
|
8094
|
+
if (!canonical) return void 0;
|
|
8095
|
+
const target = importTargetFor(canonical);
|
|
8096
|
+
if (!target) return void 0;
|
|
8097
|
+
const requirementId = stmt.vulnerability?.name ?? stmt.vulnerability?.["@id"] ?? "";
|
|
8098
|
+
if (!requirementId) return void 0;
|
|
8099
|
+
const stmtTime = (stmt.timestamp ? parseTimestamp(stmt.timestamp) : null) ?? docTime;
|
|
8100
|
+
const author = stmt.author ?? doc.author ?? "";
|
|
8101
|
+
const expiresAt = new Date(stmtTime.getTime() + DEFAULT_EXPIRY_HORIZON_MS$2);
|
|
8102
|
+
const override = {
|
|
8103
|
+
type: target.overrideType,
|
|
8104
|
+
requirementId,
|
|
8105
|
+
appliedAt: stmtTime,
|
|
8106
|
+
expiresAt,
|
|
8107
|
+
appliedBy: identityFor$1(author, doc.role),
|
|
8108
|
+
reason: buildReason$2(stmt, target.poamActionTemplate)
|
|
8109
|
+
};
|
|
8110
|
+
const affectedPackages = affectedPackagesFromIdentifiers((stmt.products ?? []).map((p) => p["@id"]).filter((id) => Boolean(id)));
|
|
8111
|
+
if (affectedPackages.length > 0) override.affectedPackages = affectedPackages;
|
|
8112
|
+
if (target.status !== void 0) override.status = target.status;
|
|
8113
|
+
if (target.setJustification && stmt.justification) {
|
|
8114
|
+
const j = normalizeJustification(stmt.justification);
|
|
8115
|
+
if (j) override.justification = j;
|
|
8116
|
+
}
|
|
8117
|
+
const ev = supplierEvidence(doc["@id"] ?? "", "OpenVEX document");
|
|
8118
|
+
if (ev) override.evidence = [ev];
|
|
8119
|
+
if (target.overrideType === OverrideType.Poam) override.milestones = [{
|
|
8120
|
+
description: target.poamActionTemplate,
|
|
8121
|
+
status: MilestoneStatus.Pending,
|
|
8122
|
+
estimatedCompletion: expiresAt
|
|
8123
|
+
}];
|
|
8124
|
+
return override;
|
|
8125
|
+
}
|
|
8126
|
+
function buildReason$2(stmt, poamTemplate) {
|
|
8127
|
+
const parts = [];
|
|
8128
|
+
if (stmt.impact_statement) parts.push(stmt.impact_statement);
|
|
8129
|
+
if (stmt.action_statement) parts.push(stmt.action_statement);
|
|
8130
|
+
if (parts.length === 0) return poamTemplate || `Imported from OpenVEX status "${stmt.status ?? ""}"`;
|
|
8131
|
+
return parts.join("\n");
|
|
8132
|
+
}
|
|
8133
|
+
function identityFor$1(author, role) {
|
|
8134
|
+
if (!author) return {
|
|
8135
|
+
type: IdentityType.System,
|
|
8136
|
+
identifier: "openvex-import"
|
|
8137
|
+
};
|
|
8138
|
+
const id = {
|
|
8139
|
+
type: author.includes("@") ? IdentityType.Email : IdentityType.Simple,
|
|
8140
|
+
identifier: author
|
|
8141
|
+
};
|
|
8142
|
+
if (role) id.description = role;
|
|
8143
|
+
return id;
|
|
8144
|
+
}
|
|
8145
|
+
function truncateId(id) {
|
|
8146
|
+
const MAX = 80;
|
|
8147
|
+
return id.length <= MAX ? id : `${id.slice(0, MAX)}...`;
|
|
8148
|
+
}
|
|
8149
|
+
//#endregion
|
|
8150
|
+
//#region converters/csaf-vex-to-hdf/typescript/converter.ts
|
|
8151
|
+
/**
|
|
8152
|
+
* CSAF VEX to HDF Amendments converter.
|
|
8153
|
+
*
|
|
8154
|
+
* CSAF (OASIS Common Security Advisory Framework) VEX profile is the
|
|
8155
|
+
* vendor-advisory format. Per the amendment-output pattern (Step 4f),
|
|
8156
|
+
* 'fixed' becomes an open POA&M, not a status flip — supplier claim is
|
|
8157
|
+
* not assessed-system evidence.
|
|
8158
|
+
*
|
|
8159
|
+
* Spec: https://docs.oasis-open.org/csaf/csaf/v2.0/csaf-v2.0.html
|
|
8160
|
+
*/
|
|
8161
|
+
const DEFAULT_EXPIRY_HORIZON_MS$1 = 365 * 24 * 60 * 60 * 1e3;
|
|
8162
|
+
async function convertCsafVexToHdf(input, converterVersion) {
|
|
8163
|
+
validateInputSize(input, "csaf-vex-to-hdf");
|
|
8164
|
+
const doc = parseJSON(input);
|
|
8165
|
+
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`);
|
|
8166
|
+
const dm = doc.document;
|
|
8167
|
+
const tracking = dm.tracking ?? {};
|
|
8168
|
+
const publisher = dm.publisher ?? {};
|
|
8169
|
+
const docTime = (tracking.current_release_date ? parseTimestamp(tracking.current_release_date) : null) ?? /* @__PURE__ */ new Date();
|
|
8170
|
+
const productLookup = buildProductLookup$1(doc.product_tree);
|
|
8171
|
+
const overrides = [];
|
|
8172
|
+
for (const v of doc.vulnerabilities ?? []) overrides.push(...vulnerabilityToOverrides(v, publisher, tracking, docTime, productLookup));
|
|
8173
|
+
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");
|
|
8174
|
+
const publisherName = publisher.name ?? "";
|
|
8175
|
+
return {
|
|
8176
|
+
name: publisherName ? `CSAF VEX statements from ${publisherName}` : "CSAF VEX statements",
|
|
8177
|
+
description: `Imported VEX advisory ${tracking.id ?? ""}`,
|
|
8178
|
+
overrides,
|
|
8179
|
+
appliedBy: identityFor(publisher),
|
|
8180
|
+
generator: {
|
|
8181
|
+
name: "csaf-vex-to-hdf",
|
|
8182
|
+
version: converterVersion
|
|
8183
|
+
},
|
|
8184
|
+
integrity: await inputIntegrity(input),
|
|
8185
|
+
version: tracking.version
|
|
8186
|
+
};
|
|
8187
|
+
}
|
|
8188
|
+
function vulnerabilityToOverrides(vuln, publisher, tracking, docTime, productLookup) {
|
|
8189
|
+
if (!vuln.cve) return [];
|
|
8190
|
+
const ps = vuln.product_status;
|
|
8191
|
+
if (!ps) return [];
|
|
8192
|
+
const out = [];
|
|
8193
|
+
if (ps.known_not_affected?.length) {
|
|
8194
|
+
const o = buildOverride(vuln, publisher, tracking, docTime, VexStatus.NotAffected, ps.known_not_affected, productLookup);
|
|
8195
|
+
if (o) out.push(o);
|
|
8196
|
+
}
|
|
8197
|
+
const fixedProducts = [...ps.fixed ?? [], ...ps.first_fixed ?? []];
|
|
8198
|
+
if (fixedProducts.length > 0) {
|
|
8199
|
+
const o = buildOverride(vuln, publisher, tracking, docTime, VexStatus.Fixed, fixedProducts, productLookup);
|
|
8200
|
+
if (o) out.push(o);
|
|
8201
|
+
}
|
|
8202
|
+
return out;
|
|
8203
|
+
}
|
|
8204
|
+
function buildOverride(vuln, publisher, tracking, docTime, canonical, products, productLookup) {
|
|
8205
|
+
const target = importTargetFor(canonical);
|
|
8206
|
+
if (!target) return void 0;
|
|
8207
|
+
const expiresAt = new Date(docTime.getTime() + DEFAULT_EXPIRY_HORIZON_MS$1);
|
|
8208
|
+
const override = {
|
|
8209
|
+
type: target.overrideType,
|
|
8210
|
+
requirementId: vuln.cve,
|
|
8211
|
+
appliedAt: docTime,
|
|
8212
|
+
expiresAt,
|
|
8213
|
+
appliedBy: identityFor(publisher),
|
|
8214
|
+
reason: buildReason$1(vuln, products)
|
|
8215
|
+
};
|
|
8216
|
+
const affectedPackages = resolveAffectedPackages(products, productLookup);
|
|
8217
|
+
if (affectedPackages.length > 0) override.affectedPackages = affectedPackages;
|
|
8218
|
+
if (target.status !== void 0) override.status = target.status;
|
|
8219
|
+
if (target.setJustification) {
|
|
8220
|
+
const j = pickJustification(vuln, products);
|
|
8221
|
+
if (j) override.justification = j;
|
|
8222
|
+
}
|
|
8223
|
+
const evidence = [];
|
|
8224
|
+
const advisoryEv = supplierEvidence(advisoryURI(publisher, tracking), "CSAF VEX advisory");
|
|
8225
|
+
if (advisoryEv) evidence.push(advisoryEv);
|
|
8226
|
+
for (const r of vuln.references ?? []) {
|
|
8227
|
+
if (!r.url) continue;
|
|
8228
|
+
const ev = supplierEvidence(r.url, r.summary ?? r.category ?? "");
|
|
8229
|
+
if (ev) evidence.push(ev);
|
|
8230
|
+
}
|
|
8231
|
+
if (evidence.length > 0) override.evidence = evidence;
|
|
8232
|
+
if (target.overrideType === OverrideType.Poam) override.milestones = [{
|
|
8233
|
+
description: firstActionRemediation(vuln, products) || target.poamActionTemplate,
|
|
8234
|
+
status: MilestoneStatus.Pending,
|
|
8235
|
+
estimatedCompletion: expiresAt
|
|
8236
|
+
}];
|
|
8237
|
+
return override;
|
|
8238
|
+
}
|
|
8239
|
+
function pickJustification(vuln, products) {
|
|
8240
|
+
const scope = new Set(products);
|
|
8241
|
+
for (const f of vuln.flags ?? []) {
|
|
8242
|
+
if (!overlap(f.product_ids ?? [], scope)) continue;
|
|
8243
|
+
if (!f.label) continue;
|
|
8244
|
+
const j = normalizeJustification(f.label);
|
|
8245
|
+
if (j) return j;
|
|
8246
|
+
}
|
|
8247
|
+
}
|
|
8248
|
+
function firstActionRemediation(vuln, products) {
|
|
8249
|
+
const scope = new Set(products);
|
|
8250
|
+
for (const r of vuln.remediations ?? []) {
|
|
8251
|
+
const ids = r.product_ids;
|
|
8252
|
+
if (ids && ids.length > 0 && !overlap(ids, scope)) continue;
|
|
8253
|
+
if ((r.category === "vendor_fix" || r.category === "mitigation" || r.category === "workaround") && r.details) return r.details;
|
|
8254
|
+
}
|
|
8255
|
+
return "";
|
|
8256
|
+
}
|
|
8257
|
+
function buildReason$1(vuln, products) {
|
|
8258
|
+
const parts = [];
|
|
8259
|
+
const scope = new Set(products);
|
|
8260
|
+
for (const n of vuln.notes ?? []) if (n.category === "description" && n.text) parts.push(n.text);
|
|
8261
|
+
for (const t of vuln.threats ?? []) {
|
|
8262
|
+
if (!t.details) continue;
|
|
8263
|
+
const ids = t.product_ids;
|
|
8264
|
+
if (ids && ids.length > 0 && !overlap(ids, scope)) continue;
|
|
8265
|
+
parts.push(t.details);
|
|
8266
|
+
}
|
|
8267
|
+
if (parts.length === 0) return `Imported from CSAF VEX (${vuln.cve ?? "unknown CVE"})`;
|
|
8268
|
+
return parts.join("\n");
|
|
8269
|
+
}
|
|
8270
|
+
/**
|
|
8271
|
+
* Walk a CSAF product_tree and build a lookup from product_id to a
|
|
8272
|
+
* structured AffectedPackage. Prefers product_identification_helper.purl,
|
|
8273
|
+
* then .cpe, then the full_product_name's `name` (used for matching only
|
|
8274
|
+
* — name+version is hard to derive from CSAF reliably, so we drop entries
|
|
8275
|
+
* with no purl and no cpe).
|
|
8276
|
+
*/
|
|
8277
|
+
function buildProductLookup$1(tree) {
|
|
8278
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
8279
|
+
if (!tree) return lookup;
|
|
8280
|
+
if (tree.full_product_names) for (const fp of tree.full_product_names) registerProduct(fp, lookup);
|
|
8281
|
+
if (tree.branches) walkBranches(tree.branches, lookup);
|
|
8282
|
+
return lookup;
|
|
8283
|
+
}
|
|
8284
|
+
function walkBranches(branches, lookup, ctx = {}) {
|
|
8285
|
+
for (const b of branches) {
|
|
8286
|
+
const next = { ...ctx };
|
|
8287
|
+
/* c8 ignore next 2 — falsy branches require a malformed CSAF branch
|
|
8288
|
+
node (category set but name empty) which we don't fixture. */
|
|
8289
|
+
if (b.category === "product_name" && b.name) next.productName = b.name;
|
|
8290
|
+
if (b.category === "product_version" && b.name) next.version = b.name;
|
|
8291
|
+
if (b.product) registerProduct(b.product, lookup, next);
|
|
8292
|
+
if (b.branches) walkBranches(b.branches, lookup, next);
|
|
8293
|
+
}
|
|
8294
|
+
}
|
|
8295
|
+
function registerProduct(fp, lookup, ctx = {}) {
|
|
8296
|
+
/* c8 ignore next — defensive against a malformed CSAF full_product_name
|
|
8297
|
+
missing its required product_id field; not fixtured. */
|
|
8298
|
+
if (!fp.product_id) return;
|
|
8299
|
+
const helper = fp.product_identification_helper;
|
|
8300
|
+
if (helper?.purl) {
|
|
8301
|
+
const pkg = affectedPackageFromIdentifier(helper.purl);
|
|
8302
|
+
/* c8 ignore next 4 — affectedPackageFromIdentifier always returns a
|
|
8303
|
+
non-null value for an input that starts with 'pkg:' (preserves
|
|
8304
|
+
malformed input as purl-only). The null branch is unreachable from
|
|
8305
|
+
this callsite. */
|
|
8306
|
+
if (pkg) {
|
|
8307
|
+
lookup.set(fp.product_id, pkg);
|
|
8308
|
+
return;
|
|
8309
|
+
}
|
|
8310
|
+
}
|
|
8311
|
+
if (helper?.cpe) {
|
|
8312
|
+
const pkg = affectedPackageFromIdentifier(helper.cpe);
|
|
8313
|
+
/* c8 ignore next 4 — same as above; identifiers starting with
|
|
8314
|
+
'cpe:2.3:' always produce a cpe-only AffectedPackage. */
|
|
8315
|
+
if (pkg) {
|
|
8316
|
+
lookup.set(fp.product_id, pkg);
|
|
8317
|
+
return;
|
|
8318
|
+
}
|
|
8319
|
+
}
|
|
8320
|
+
if (ctx.productName && ctx.version) lookup.set(fp.product_id, {
|
|
8321
|
+
name: ctx.productName,
|
|
8322
|
+
version: ctx.version,
|
|
8323
|
+
ecosystem: Ecosystem.Generic
|
|
8324
|
+
});
|
|
8325
|
+
}
|
|
8326
|
+
function resolveAffectedPackages(productIds, lookup) {
|
|
8327
|
+
const out = [];
|
|
8328
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
8329
|
+
for (const id of productIds) {
|
|
8330
|
+
const pkg = lookup.get(id);
|
|
8331
|
+
if (!pkg) continue;
|
|
8332
|
+
const key = pkg.purl ?? pkg.cpe ?? `${pkg.name ?? ""}@${pkg.version ?? ""}`;
|
|
8333
|
+
if (seenKeys.has(key)) continue;
|
|
8334
|
+
seenKeys.add(key);
|
|
8335
|
+
out.push(pkg);
|
|
8336
|
+
}
|
|
8337
|
+
return out;
|
|
8338
|
+
}
|
|
8339
|
+
function identityFor(p) {
|
|
8340
|
+
if (!p.name) return {
|
|
8341
|
+
type: IdentityType.System,
|
|
8342
|
+
identifier: "csaf-vex-import"
|
|
8343
|
+
};
|
|
8344
|
+
const id = {
|
|
8345
|
+
type: IdentityType.Simple,
|
|
8346
|
+
identifier: p.name
|
|
8347
|
+
};
|
|
8348
|
+
if (p.category) id.description = p.category;
|
|
8349
|
+
return id;
|
|
8350
|
+
}
|
|
8351
|
+
function advisoryURI(publisher, tracking) {
|
|
8352
|
+
/* c8 ignore next */
|
|
8353
|
+
const id = tracking.id ?? "";
|
|
8354
|
+
const ns = publisher.namespace;
|
|
8355
|
+
if (ns && id) return `${ns.replace(/\/+$/, "")}/${id}`;
|
|
8356
|
+
return id;
|
|
8357
|
+
}
|
|
8358
|
+
function overlap(ids, scope) {
|
|
8359
|
+
return ids.some((id) => scope.has(id));
|
|
8360
|
+
}
|
|
8361
|
+
//#endregion
|
|
8362
|
+
//#region converters/cyclonedx-vex-to-hdf/typescript/converter.ts
|
|
8363
|
+
/**
|
|
8364
|
+
* CycloneDX VEX to HDF Amendments converter.
|
|
8365
|
+
*
|
|
8366
|
+
* CycloneDX VEX is not a separate format — it's a CycloneDX BOM whose
|
|
8367
|
+
* vulnerabilities[] carry an `analysis` object. CycloneDX-specific
|
|
8368
|
+
* justifications without an HDF equivalent (requires_configuration,
|
|
8369
|
+
* protected_by_compiler, etc.) are preserved verbatim in the reason
|
|
8370
|
+
* field via the shared helper's unknown-value passthrough.
|
|
8371
|
+
*
|
|
8372
|
+
* Spec: https://cyclonedx.org/use-cases/#vulnerability-exploitability
|
|
8373
|
+
*/
|
|
8374
|
+
const DEFAULT_EXPIRY_HORIZON_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
8375
|
+
async function convertCyclonedxVexToHdf(input, converterVersion) {
|
|
8376
|
+
validateInputSize(input, "cyclonedx-vex-to-hdf");
|
|
8377
|
+
const bom = parseJSON(input);
|
|
8378
|
+
if (bom.bomFormat !== "CycloneDX") throw new Error(`cyclonedx-vex-to-hdf: bomFormat is ${JSON.stringify(bom.bomFormat)}; only 'CycloneDX' is supported`);
|
|
8379
|
+
const docTime = (bom.metadata?.timestamp ? parseTimestamp(bom.metadata.timestamp) : null) ?? /* @__PURE__ */ new Date();
|
|
8380
|
+
const productLookup = buildProductLookup(bom);
|
|
8381
|
+
const publisher = publisherIdentityOrDefault(bom);
|
|
8382
|
+
const overrides = [];
|
|
8383
|
+
for (const v of bom.vulnerabilities ?? []) {
|
|
8384
|
+
const o = vulnerabilityToOverride(v, productLookup, docTime, publisher);
|
|
8385
|
+
if (o) overrides.push(o);
|
|
8386
|
+
}
|
|
8387
|
+
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");
|
|
8388
|
+
const publisherName = publisher.identifier;
|
|
8389
|
+
return {
|
|
8390
|
+
name: publisherName && publisherName !== "cyclonedx-vex-import" ? `CycloneDX VEX statements from ${publisherName}` : "CycloneDX VEX statements",
|
|
8391
|
+
description: bom.serialNumber ? `Imported CycloneDX VEX ${bom.serialNumber}` : "Imported CycloneDX VEX",
|
|
8392
|
+
overrides,
|
|
8393
|
+
appliedBy: publisher,
|
|
8394
|
+
generator: {
|
|
8395
|
+
name: "cyclonedx-vex-to-hdf",
|
|
8396
|
+
version: converterVersion
|
|
8397
|
+
},
|
|
8398
|
+
integrity: await inputIntegrity(input)
|
|
8399
|
+
};
|
|
8400
|
+
}
|
|
8401
|
+
function vulnerabilityToOverride(v, productLookup, docTime, publisher) {
|
|
8402
|
+
if (!v.id || !v.analysis?.state) return void 0;
|
|
8403
|
+
const canonical = normalizeStatus(v.analysis.state);
|
|
8404
|
+
if (!canonical) return void 0;
|
|
8405
|
+
const target = importTargetFor(canonical);
|
|
8406
|
+
if (!target) return void 0;
|
|
8407
|
+
const affectedPackages = affectedPackagesForVuln(v, productLookup);
|
|
8408
|
+
const expiresAt = new Date(docTime.getTime() + DEFAULT_EXPIRY_HORIZON_MS);
|
|
8409
|
+
const override = {
|
|
8410
|
+
type: target.overrideType,
|
|
8411
|
+
requirementId: v.id,
|
|
8412
|
+
appliedAt: docTime,
|
|
8413
|
+
expiresAt,
|
|
8414
|
+
appliedBy: publisher,
|
|
8415
|
+
reason: buildReason(v)
|
|
8416
|
+
};
|
|
8417
|
+
if (affectedPackages.length > 0) override.affectedPackages = affectedPackages;
|
|
8418
|
+
if (target.status !== void 0) override.status = target.status;
|
|
8419
|
+
if (target.setJustification && v.analysis.justification) {
|
|
8420
|
+
const j = normalizeJustification(v.analysis.justification);
|
|
8421
|
+
if (j) override.justification = j;
|
|
8422
|
+
}
|
|
8423
|
+
const evidence = [];
|
|
8424
|
+
if (v.source?.url) {
|
|
8425
|
+
const ev = supplierEvidence(v.source.url, v.source.name ?? "");
|
|
8426
|
+
if (ev) evidence.push(ev);
|
|
8427
|
+
}
|
|
8428
|
+
for (const r of v.references ?? []) {
|
|
8429
|
+
if (!r.source?.url) continue;
|
|
8430
|
+
const ev = supplierEvidence(r.source.url, r.source.name ?? "");
|
|
8431
|
+
if (ev) evidence.push(ev);
|
|
8432
|
+
}
|
|
8433
|
+
if (evidence.length > 0) override.evidence = evidence;
|
|
8434
|
+
if (target.overrideType === OverrideType.Poam) override.milestones = [{
|
|
8435
|
+
description: firstActionFromResponse(v.analysis.response ?? []) || target.poamActionTemplate,
|
|
8436
|
+
status: MilestoneStatus.Pending,
|
|
8437
|
+
estimatedCompletion: expiresAt
|
|
8438
|
+
}];
|
|
8439
|
+
return override;
|
|
8440
|
+
}
|
|
8441
|
+
function buildReason(v) {
|
|
8442
|
+
const parts = [];
|
|
8443
|
+
if (v.description) parts.push(v.description);
|
|
8444
|
+
if (v.analysis?.detail) parts.push(v.analysis.detail);
|
|
8445
|
+
return parts.join("\n");
|
|
8446
|
+
}
|
|
8447
|
+
/**
|
|
8448
|
+
* Resolve CycloneDX affects[].ref entries to structured AffectedPackage
|
|
8449
|
+
* entries. Looks up the bom-ref in the component table to recover purl,
|
|
8450
|
+
* name/version. Opaque bom-refs with no component-table match are dropped
|
|
8451
|
+
* (the schema forbids fabricating name+version, and bom-refs aren't
|
|
8452
|
+
* portable identifiers outside their source BOM).
|
|
8453
|
+
*/
|
|
8454
|
+
function affectedPackagesForVuln(v, lookup) {
|
|
8455
|
+
const out = [];
|
|
8456
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
8457
|
+
for (const a of v.affects ?? []) {
|
|
8458
|
+
if (!a.ref) continue;
|
|
8459
|
+
const comp = lookup.get(a.ref);
|
|
8460
|
+
const pkg = comp ? affectedPackageFromComponent(comp) : affectedPackageFromIdentifier(a.ref);
|
|
8461
|
+
if (!pkg) continue;
|
|
8462
|
+
const key = pkg.purl ?? pkg.cpe ?? `${pkg.name ?? ""}@${pkg.version ?? ""}`;
|
|
8463
|
+
if (seenKeys.has(key)) continue;
|
|
8464
|
+
seenKeys.add(key);
|
|
8465
|
+
out.push(pkg);
|
|
8466
|
+
}
|
|
8467
|
+
return out;
|
|
8468
|
+
}
|
|
8469
|
+
/**
|
|
8470
|
+
* Build an AffectedPackage from a CycloneDX Component. Prefers structured
|
|
8471
|
+
* purl decomposition via the shared helper (so name/version/ecosystem are
|
|
8472
|
+
* also populated when the purl is parseable); falls back to the component's
|
|
8473
|
+
* name+version when no purl is set. Returns undefined when the component
|
|
8474
|
+
* carries neither a purl nor a name+version pair (schema requires at least
|
|
8475
|
+
* name+version+ecosystem, purl, or cpe).
|
|
8476
|
+
*/
|
|
8477
|
+
function affectedPackageFromComponent(c) {
|
|
8478
|
+
if (c.purl) return affectedPackageFromIdentifier(c.purl);
|
|
8479
|
+
if (c.name && c.version) return {
|
|
8480
|
+
name: c.name,
|
|
8481
|
+
version: c.version,
|
|
8482
|
+
ecosystem: Ecosystem.Generic
|
|
8483
|
+
};
|
|
8484
|
+
}
|
|
8485
|
+
function buildProductLookup(bom) {
|
|
8486
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
8487
|
+
const root = bom.metadata?.component;
|
|
8488
|
+
if (root?.["bom-ref"]) lookup.set(root["bom-ref"], root);
|
|
8489
|
+
for (const c of bom.components ?? []) if (c["bom-ref"]) lookup.set(c["bom-ref"], c);
|
|
8490
|
+
return lookup;
|
|
8491
|
+
}
|
|
8492
|
+
function firstActionFromResponse(resp) {
|
|
8493
|
+
for (const r of resp) switch (r.trim().toLowerCase()) {
|
|
8494
|
+
case "update": return "Apply vendor update and re-scan to verify.";
|
|
8495
|
+
case "rollback": return "Roll back to the unaffected version and re-scan to verify.";
|
|
8496
|
+
case "workaround_available": return "Apply the documented workaround.";
|
|
8497
|
+
}
|
|
8498
|
+
return "";
|
|
8499
|
+
}
|
|
8500
|
+
function publisherIdentityOrDefault(bom) {
|
|
8501
|
+
for (const a of bom.metadata?.authors ?? []) {
|
|
8502
|
+
if (a.email) return {
|
|
8503
|
+
type: IdentityType.Email,
|
|
8504
|
+
identifier: a.email
|
|
8505
|
+
};
|
|
8506
|
+
if (a.name) return {
|
|
8507
|
+
type: IdentityType.Simple,
|
|
8508
|
+
identifier: a.name
|
|
8509
|
+
};
|
|
8510
|
+
}
|
|
8511
|
+
for (const t of bom.metadata?.tools ?? []) {
|
|
8512
|
+
const ident = [t.vendor, t.name].filter(Boolean).join(" ").trim();
|
|
8513
|
+
if (ident) return {
|
|
8514
|
+
type: IdentityType.System,
|
|
8515
|
+
identifier: ident
|
|
8516
|
+
};
|
|
8517
|
+
}
|
|
8518
|
+
return {
|
|
8519
|
+
type: IdentityType.System,
|
|
8520
|
+
identifier: "cyclonedx-vex-import"
|
|
8521
|
+
};
|
|
8522
|
+
}
|
|
8523
|
+
//#endregion
|
|
8524
|
+
//#region converters/hdf-to-csaf-vex/typescript/converter.ts
|
|
8525
|
+
/**
|
|
8526
|
+
* HDF Amendments to CSAF VEX (csaf_vex profile) converter.
|
|
8527
|
+
*
|
|
8528
|
+
* Reverse direction of csaf-vex-to-hdf. Intentionally partial-fidelity:
|
|
8529
|
+
* fields the shared VEX mapping considers consumer-action-bearing survive
|
|
8530
|
+
* round-trip; the rest collapse into the available CSAF fields.
|
|
8531
|
+
*/
|
|
8532
|
+
const CVE_ID_PATTERN$2 = /^CVE-\d{4}-\d{4,}$/;
|
|
8533
|
+
const PRODUCTS_LINE$2 = /^Products:\s*(.+)$/m;
|
|
8534
|
+
const DEFAULT_PRODUCT_ID$2 = "HDFPID-0001";
|
|
8535
|
+
function convertHdfToCsafVex(input, converterVersion) {
|
|
8536
|
+
validateInputSize(input, "hdf-to-csaf-vex");
|
|
8537
|
+
const amendments = parseJSON(input);
|
|
8538
|
+
const groups = groupByCVE(amendments.overrides ?? []);
|
|
8539
|
+
const productSet = /* @__PURE__ */ new Map();
|
|
8540
|
+
const vulnerabilities = [];
|
|
8541
|
+
for (const group of groups) {
|
|
8542
|
+
const v = buildVulnerability(group);
|
|
8543
|
+
if (!v) continue;
|
|
8544
|
+
vulnerabilities.push(v);
|
|
8545
|
+
for (const p of productIDsForGroup(group)) productSet.set(p, true);
|
|
8546
|
+
}
|
|
8547
|
+
if (vulnerabilities.length === 0) throw new Error("hdf-to-csaf-vex: no overrides with CVE-shaped requirementIds; nothing to emit");
|
|
8548
|
+
const products = [...productSet.keys()].sort();
|
|
8549
|
+
const doc = buildDocument(amendments, converterVersion);
|
|
8550
|
+
doc.vulnerabilities = vulnerabilities;
|
|
8551
|
+
doc.product_tree.full_product_names = products.length > 0 ? products.map((p) => ({
|
|
8552
|
+
name: p,
|
|
8553
|
+
product_id: p
|
|
8554
|
+
})) : [{
|
|
8555
|
+
name: DEFAULT_PRODUCT_ID$2,
|
|
8556
|
+
product_id: DEFAULT_PRODUCT_ID$2
|
|
8557
|
+
}];
|
|
8558
|
+
return JSON.stringify(doc, null, 2);
|
|
8559
|
+
}
|
|
8560
|
+
function groupByCVE(overrides) {
|
|
8561
|
+
const groups = /* @__PURE__ */ new Map();
|
|
8562
|
+
for (const o of overrides) {
|
|
8563
|
+
if (!CVE_ID_PATTERN$2.test(o.requirementId)) continue;
|
|
8564
|
+
let g = groups.get(o.requirementId);
|
|
8565
|
+
if (!g) {
|
|
8566
|
+
g = {
|
|
8567
|
+
cve: o.requirementId,
|
|
8568
|
+
overrides: []
|
|
8569
|
+
};
|
|
8570
|
+
groups.set(o.requirementId, g);
|
|
8571
|
+
}
|
|
8572
|
+
g.overrides.push(o);
|
|
8573
|
+
}
|
|
8574
|
+
return [...groups.values()].sort((a, b) => a.cve.localeCompare(b.cve));
|
|
8575
|
+
}
|
|
8576
|
+
function productIDsForGroup(group) {
|
|
8577
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8578
|
+
for (const o of group.overrides) for (const p of productIDsFor$1(o)) seen.add(p);
|
|
8579
|
+
return [...seen].sort();
|
|
8580
|
+
}
|
|
8581
|
+
function productIDsFor$1(o) {
|
|
8582
|
+
if (o.affectedPackages && o.affectedPackages.length > 0) {
|
|
8583
|
+
const ids = o.affectedPackages.map((p) => affectedPackageToIdentifier(p)).filter((id) => Boolean(id));
|
|
8584
|
+
if (ids.length > 0) return ids;
|
|
8585
|
+
}
|
|
8586
|
+
if (o.componentRef) return [o.componentRef];
|
|
8587
|
+
const m = PRODUCTS_LINE$2.exec(o.reason ?? "");
|
|
8588
|
+
if (m && m[1]) {
|
|
8589
|
+
const parts = m[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
8590
|
+
if (parts.length > 0) return parts;
|
|
8591
|
+
}
|
|
8592
|
+
return [DEFAULT_PRODUCT_ID$2];
|
|
8593
|
+
}
|
|
8594
|
+
function stripProductsLine$1(reason) {
|
|
8595
|
+
return reason.replace(PRODUCTS_LINE$2, "").replace(/\n+$/, "");
|
|
8596
|
+
}
|
|
8597
|
+
function allMilestonesCompleted$2(o) {
|
|
8598
|
+
if (!o.milestones || o.milestones.length === 0) return false;
|
|
8599
|
+
return o.milestones.every((m) => m.status === MilestoneStatus.Completed);
|
|
8600
|
+
}
|
|
8601
|
+
function buildVulnerability(group) {
|
|
8602
|
+
const v = { cve: group.cve };
|
|
8603
|
+
const status = {};
|
|
8604
|
+
let emitted = false;
|
|
8605
|
+
for (const o of group.overrides) {
|
|
8606
|
+
const pids = productIDsFor$1(o);
|
|
8607
|
+
let canonical = exportStatusFor(o, allMilestonesCompleted$2(o), false);
|
|
8608
|
+
if (!canonical) continue;
|
|
8609
|
+
if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted$2(o)) canonical = VexStatus.Fixed;
|
|
8610
|
+
if (canonical === VexStatus.NotAffected) {
|
|
8611
|
+
status.known_not_affected = (status.known_not_affected ?? []).concat(pids);
|
|
8612
|
+
if (o.justification) {
|
|
8613
|
+
v.flags = v.flags ?? [];
|
|
8614
|
+
v.flags.push({
|
|
8615
|
+
label: String(o.justification),
|
|
8616
|
+
product_ids: pids,
|
|
8617
|
+
date: new Date(o.appliedAt).toISOString().replace(/\.\d+Z$/, "Z")
|
|
8618
|
+
});
|
|
8619
|
+
}
|
|
8620
|
+
emitted = true;
|
|
8621
|
+
} else if (canonical === VexStatus.Fixed) {
|
|
8622
|
+
status.fixed = (status.fixed ?? []).concat(pids);
|
|
8623
|
+
for (const m of o.milestones ?? []) {
|
|
8624
|
+
if (!m.description) continue;
|
|
8625
|
+
v.remediations = v.remediations ?? [];
|
|
8626
|
+
v.remediations.push({
|
|
8627
|
+
category: "vendor_fix",
|
|
8628
|
+
details: m.description,
|
|
8629
|
+
product_ids: pids
|
|
8630
|
+
});
|
|
8631
|
+
}
|
|
8632
|
+
emitted = true;
|
|
8633
|
+
} else if (canonical === VexStatus.Affected) {
|
|
8634
|
+
status.known_affected = (status.known_affected ?? []).concat(pids);
|
|
8635
|
+
if (o.reason) {
|
|
8636
|
+
v.threats = v.threats ?? [];
|
|
8637
|
+
v.threats.push({
|
|
8638
|
+
category: "impact",
|
|
8639
|
+
details: stripProductsLine$1(o.reason),
|
|
8640
|
+
product_ids: pids
|
|
8641
|
+
});
|
|
8642
|
+
}
|
|
8643
|
+
if (o.type === OverrideType.Poam) for (const m of o.milestones ?? []) {
|
|
8644
|
+
if (!m.description) continue;
|
|
8645
|
+
v.remediations = v.remediations ?? [];
|
|
8646
|
+
v.remediations.push({
|
|
8647
|
+
category: "workaround",
|
|
8648
|
+
details: m.description,
|
|
8649
|
+
product_ids: pids
|
|
8650
|
+
});
|
|
8651
|
+
}
|
|
8652
|
+
emitted = true;
|
|
8653
|
+
}
|
|
8654
|
+
for (const e of o.evidence ?? []) {
|
|
8655
|
+
if (e.type !== "url" || !e.data) continue;
|
|
8656
|
+
v.references = v.references ?? [];
|
|
8657
|
+
v.references.push({
|
|
8658
|
+
category: "external",
|
|
8659
|
+
summary: e.description ?? "",
|
|
8660
|
+
url: e.data
|
|
8661
|
+
});
|
|
8662
|
+
}
|
|
8663
|
+
}
|
|
8664
|
+
/* c8 ignore next */
|
|
8665
|
+
if (!emitted) return void 0;
|
|
8666
|
+
if (status.fixed) status.fixed = [...new Set(status.fixed)];
|
|
8667
|
+
if (status.known_affected) status.known_affected = [...new Set(status.known_affected)];
|
|
8668
|
+
if (status.known_not_affected) status.known_not_affected = [...new Set(status.known_not_affected)];
|
|
8669
|
+
v.product_status = status;
|
|
8670
|
+
if (v.references) {
|
|
8671
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8672
|
+
v.references = v.references.filter((r) => {
|
|
8673
|
+
if (seen.has(r.url)) return false;
|
|
8674
|
+
seen.add(r.url);
|
|
8675
|
+
return true;
|
|
8676
|
+
});
|
|
8677
|
+
}
|
|
8678
|
+
return v;
|
|
8679
|
+
}
|
|
8680
|
+
function buildDocument(amendments, converterVersion) {
|
|
8681
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
|
|
8682
|
+
const publisherName = amendments.appliedBy?.identifier || "HDF Amendments Export";
|
|
8683
|
+
const trackingId = amendments.amendmentId || "HDF-VEX-EXPORT";
|
|
8684
|
+
const docVersion = amendments.version || "1";
|
|
8685
|
+
const title = amendments.name || "HDF Amendments exported as CSAF VEX";
|
|
8686
|
+
const notes = [];
|
|
8687
|
+
if (amendments.description) notes.push({
|
|
8688
|
+
category: "summary",
|
|
8689
|
+
text: amendments.description,
|
|
8690
|
+
title: "Description"
|
|
8691
|
+
});
|
|
8692
|
+
return {
|
|
8693
|
+
document: {
|
|
8694
|
+
category: "csaf_vex",
|
|
8695
|
+
csaf_version: "2.0",
|
|
8696
|
+
title,
|
|
8697
|
+
notes,
|
|
8698
|
+
publisher: {
|
|
8699
|
+
category: "other",
|
|
8700
|
+
name: publisherName
|
|
8701
|
+
},
|
|
8702
|
+
tracking: {
|
|
8703
|
+
id: trackingId,
|
|
8704
|
+
status: "final",
|
|
8705
|
+
version: docVersion,
|
|
8706
|
+
current_release_date: now,
|
|
8707
|
+
initial_release_date: now,
|
|
8708
|
+
revision_history: [{
|
|
8709
|
+
date: now,
|
|
8710
|
+
number: docVersion,
|
|
8711
|
+
summary: "Generated by hdf-to-csaf-vex from HDF Amendments."
|
|
8712
|
+
}],
|
|
8713
|
+
generator: {
|
|
8714
|
+
engine: {
|
|
8715
|
+
name: "hdf-to-csaf-vex",
|
|
8716
|
+
version: converterVersion
|
|
8717
|
+
},
|
|
8718
|
+
date: now
|
|
8719
|
+
}
|
|
8720
|
+
}
|
|
8721
|
+
},
|
|
8722
|
+
product_tree: { full_product_names: [] },
|
|
8723
|
+
vulnerabilities: []
|
|
8724
|
+
};
|
|
8725
|
+
}
|
|
8726
|
+
//#endregion
|
|
8727
|
+
//#region converters/hdf-to-openvex/typescript/converter.ts
|
|
8728
|
+
/**
|
|
8729
|
+
* HDF Amendments to OpenVEX converter.
|
|
8730
|
+
*
|
|
8731
|
+
* Reverse direction of openvex-to-hdf. Step 4f amendment-output pattern,
|
|
8732
|
+
* partial-fidelity by design — consumer-action-bearing fields (CVE id,
|
|
8733
|
+
* status, justification) survive round-trip; the rest collapse.
|
|
8734
|
+
*/
|
|
8735
|
+
const CVE_ID_PATTERN$1 = /^CVE-\d{4}-\d{4,}$/;
|
|
8736
|
+
const PRODUCTS_LINE$1 = /^Products:\s*(.+)$/m;
|
|
8737
|
+
const OPENVEX_CONTEXT = "https://openvex.dev/ns/v0.2.0";
|
|
8738
|
+
const OPENVEX_NAMESPACE = "https://openvex.dev/docs/public/";
|
|
8739
|
+
const DEFAULT_PRODUCT_ID$1 = "HDFPID-0001";
|
|
8740
|
+
async function convertHdfToOpenVex(input, converterVersion) {
|
|
8741
|
+
validateInputSize(input, "hdf-to-openvex");
|
|
8742
|
+
const amendments = parseJSON(input);
|
|
8743
|
+
const statements = [];
|
|
8744
|
+
let earliest;
|
|
8745
|
+
for (const o of amendments.overrides ?? []) {
|
|
8746
|
+
const s = overrideToStatement(o);
|
|
8747
|
+
if (!s) continue;
|
|
8748
|
+
statements.push(s);
|
|
8749
|
+
const t = new Date(o.appliedAt);
|
|
8750
|
+
if (!earliest || t < earliest) earliest = t;
|
|
8751
|
+
}
|
|
8752
|
+
if (statements.length === 0) throw new Error("hdf-to-openvex: no overrides with CVE-shaped requirementIds; nothing to emit");
|
|
8753
|
+
statements.sort((a, b) => a.vulnerability.name.localeCompare(b.vulnerability.name));
|
|
8754
|
+
const author = amendments.appliedBy?.identifier || "HDF Amendments Export";
|
|
8755
|
+
const role = amendments.appliedBy?.description;
|
|
8756
|
+
const doc = {
|
|
8757
|
+
"@context": OPENVEX_CONTEXT,
|
|
8758
|
+
"@id": await buildDocumentID(input, amendments),
|
|
8759
|
+
author,
|
|
8760
|
+
role,
|
|
8761
|
+
timestamp: (earliest ?? /* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z"),
|
|
8762
|
+
version: 1,
|
|
8763
|
+
statements
|
|
8764
|
+
};
|
|
8765
|
+
return JSON.stringify(doc, null, 2);
|
|
8766
|
+
}
|
|
8767
|
+
function overrideToStatement(o) {
|
|
8768
|
+
if (!CVE_ID_PATTERN$1.test(o.requirementId)) return void 0;
|
|
8769
|
+
let canonical = exportStatusFor(o, allMilestonesCompleted$1(o), false);
|
|
8770
|
+
if (!canonical) return void 0;
|
|
8771
|
+
if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted$1(o)) canonical = VexStatus.Fixed;
|
|
8772
|
+
const stmt = {
|
|
8773
|
+
vulnerability: {
|
|
8774
|
+
name: o.requirementId,
|
|
8775
|
+
"@id": `https://nvd.nist.gov/vuln/detail/${o.requirementId}`
|
|
8776
|
+
},
|
|
8777
|
+
status: String(canonical),
|
|
8778
|
+
timestamp: new Date(o.appliedAt).toISOString().replace(/\.\d+Z$/, "Z"),
|
|
8779
|
+
products: productsFor(o)
|
|
8780
|
+
};
|
|
8781
|
+
if (canonical === VexStatus.NotAffected) {
|
|
8782
|
+
if (o.justification) stmt.justification = String(o.justification);
|
|
8783
|
+
const impact = stripProductsLine(o.reason ?? "");
|
|
8784
|
+
if (impact) stmt.impact_statement = impact;
|
|
8785
|
+
} else if (canonical === VexStatus.Fixed) stmt.action_statement = firstMilestoneAction(o) || "Fix applied; consumer re-scan confirmed clean.";
|
|
8786
|
+
else if (canonical === VexStatus.Affected) stmt.action_statement = firstMilestoneAction(o) || stripProductsLine(o.reason ?? "");
|
|
8787
|
+
return stmt;
|
|
8788
|
+
}
|
|
8789
|
+
function productsFor(o) {
|
|
8790
|
+
if (o.affectedPackages && o.affectedPackages.length > 0) {
|
|
8791
|
+
const ids = o.affectedPackages.map((p) => affectedPackageToIdentifier(p)).filter((id) => Boolean(id));
|
|
8792
|
+
if (ids.length > 0) return ids.map((id) => ({ "@id": id }));
|
|
8793
|
+
}
|
|
8794
|
+
let ids = [];
|
|
8795
|
+
if (o.componentRef) ids = [o.componentRef];
|
|
8796
|
+
else {
|
|
8797
|
+
const m = PRODUCTS_LINE$1.exec(o.reason ?? "");
|
|
8798
|
+
if (m && m[1]) ids = m[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
8799
|
+
}
|
|
8800
|
+
if (ids.length === 0) ids = [DEFAULT_PRODUCT_ID$1];
|
|
8801
|
+
return ids.map((id) => ({ "@id": id }));
|
|
8802
|
+
}
|
|
8803
|
+
function firstMilestoneAction(o) {
|
|
8804
|
+
for (const m of o.milestones ?? []) if (m.description) return m.description;
|
|
8805
|
+
return "";
|
|
8806
|
+
}
|
|
8807
|
+
function allMilestonesCompleted$1(o) {
|
|
8808
|
+
if (!o.milestones || o.milestones.length === 0) return false;
|
|
8809
|
+
return o.milestones.every((m) => m.status === MilestoneStatus.Completed);
|
|
8810
|
+
}
|
|
8811
|
+
function stripProductsLine(reason) {
|
|
8812
|
+
return reason.replace(PRODUCTS_LINE$1, "").replace(/\n+$/, "");
|
|
8813
|
+
}
|
|
8814
|
+
async function buildDocumentID(input, a) {
|
|
8815
|
+
if (a.amendmentId) return `${OPENVEX_NAMESPACE}vex-${a.amendmentId}`;
|
|
8816
|
+
return `${OPENVEX_NAMESPACE}vex-${await sha256(input)}`;
|
|
8817
|
+
}
|
|
8818
|
+
//#endregion
|
|
8819
|
+
//#region converters/hdf-to-cyclonedx-vex/typescript/converter.ts
|
|
8820
|
+
/**
|
|
8821
|
+
* HDF Amendments to CycloneDX VEX converter (export side).
|
|
8822
|
+
*
|
|
8823
|
+
* Reverse direction of cyclonedx-vex-to-hdf. Step 4f amendment-output
|
|
8824
|
+
* pattern, partial-fidelity by design — consumer-action-bearing fields
|
|
8825
|
+
* (CVE id, status, justification) survive round-trip; the rest collapse
|
|
8826
|
+
* into the available CycloneDX VEX fields.
|
|
8827
|
+
*/
|
|
8828
|
+
const CVE_ID_PATTERN = /^CVE-\d{4}-\d{4,}$/;
|
|
8829
|
+
const PRODUCTS_LINE = /^Products:\s*(.+)$/m;
|
|
8830
|
+
const RAW_JUST_LINE = /^VEX justification:\s*(.+)$/m;
|
|
8831
|
+
const RESPONSE_LINE = /^Response:.*$/gm;
|
|
8832
|
+
const DEFAULT_PRODUCT_ID = "HDFPID-0001";
|
|
8833
|
+
function convertHdfToCyclonedxVex(input, converterVersion) {
|
|
8834
|
+
validateInputSize(input, "hdf-to-cyclonedx-vex");
|
|
8835
|
+
const amendments = parseJSON(input);
|
|
8836
|
+
const componentRegistry = /* @__PURE__ */ new Map();
|
|
8837
|
+
const vulnerabilities = [];
|
|
8838
|
+
let earliest;
|
|
8839
|
+
for (const o of amendments.overrides ?? []) {
|
|
8840
|
+
if (!CVE_ID_PATTERN.test(o.requirementId)) continue;
|
|
8841
|
+
const v = overrideToVulnerability(o, componentRegistry);
|
|
8842
|
+
if (!v) continue;
|
|
8843
|
+
vulnerabilities.push(v);
|
|
8844
|
+
const t = new Date(o.appliedAt);
|
|
8845
|
+
if (!earliest || t < earliest) earliest = t;
|
|
8846
|
+
}
|
|
8847
|
+
if (vulnerabilities.length === 0) throw new Error("hdf-to-cyclonedx-vex: no overrides with CVE-shaped requirementIds; nothing to emit");
|
|
8848
|
+
vulnerabilities.sort((a, b) => a.id.localeCompare(b.id));
|
|
8849
|
+
const components = [...componentRegistry.values()].sort((a, b) => a["bom-ref"].localeCompare(b["bom-ref"]));
|
|
8850
|
+
const bom = {
|
|
8851
|
+
bomFormat: "CycloneDX",
|
|
8852
|
+
specVersion: "1.4",
|
|
8853
|
+
serialNumber: buildSerialNumberSync(input, amendments),
|
|
8854
|
+
version: 1,
|
|
8855
|
+
metadata: buildMetadata(amendments, earliest ?? /* @__PURE__ */ new Date(), converterVersion),
|
|
8856
|
+
components,
|
|
8857
|
+
vulnerabilities
|
|
8858
|
+
};
|
|
8859
|
+
return JSON.stringify(bom, null, 2);
|
|
8860
|
+
}
|
|
8861
|
+
function overrideToVulnerability(o, componentRegistry) {
|
|
8862
|
+
let canonical = exportStatusFor(o, allMilestonesCompleted(o), false);
|
|
8863
|
+
if (!canonical) return void 0;
|
|
8864
|
+
if (o.type === OverrideType.Poam && canonical === VexStatus.Affected && allMilestonesCompleted(o)) canonical = VexStatus.Fixed;
|
|
8865
|
+
const pids = productIDsFor(o);
|
|
8866
|
+
const pkgById = /* @__PURE__ */ new Map();
|
|
8867
|
+
for (const p of o.affectedPackages ?? []) {
|
|
8868
|
+
const id = affectedPackageToIdentifier(p);
|
|
8869
|
+
if (id) pkgById.set(id, p);
|
|
8870
|
+
}
|
|
8871
|
+
for (const pid of pids) componentRegistry.set(pid, componentFor(pid, pkgById.get(pid)));
|
|
8872
|
+
const analysis = { state: canonicalToCycloneDXState(canonical) };
|
|
8873
|
+
if (o.justification) {
|
|
8874
|
+
const cdxJust = justificationForCycloneDX(o.justification);
|
|
8875
|
+
if (cdxJust) analysis.justification = cdxJust;
|
|
8876
|
+
}
|
|
8877
|
+
const detail = stripReasonAnnotations(o.reason ?? "");
|
|
8878
|
+
if (detail) analysis.detail = detail;
|
|
8879
|
+
if (canonical === VexStatus.Fixed) analysis.response = ["update"];
|
|
8880
|
+
else if (canonical === VexStatus.Affected && o.type === OverrideType.Poam) analysis.response = ["workaround_available"];
|
|
8881
|
+
const v = {
|
|
8882
|
+
id: o.requirementId,
|
|
8883
|
+
source: {
|
|
8884
|
+
name: "NVD",
|
|
8885
|
+
url: `https://nvd.nist.gov/vuln/detail/${o.requirementId}`
|
|
8886
|
+
},
|
|
8887
|
+
analysis,
|
|
8888
|
+
affects: pids.map((p) => ({ ref: p }))
|
|
8889
|
+
};
|
|
8890
|
+
for (const e of o.evidence ?? []) {
|
|
8891
|
+
if (e.type !== "url" || !e.data) continue;
|
|
8892
|
+
v.references = v.references ?? [];
|
|
8893
|
+
v.references.push({
|
|
8894
|
+
id: o.requirementId,
|
|
8895
|
+
source: {
|
|
8896
|
+
name: e.description ?? "",
|
|
8897
|
+
url: e.data
|
|
8898
|
+
}
|
|
8899
|
+
});
|
|
8900
|
+
}
|
|
8901
|
+
return v;
|
|
8902
|
+
}
|
|
8903
|
+
function canonicalToCycloneDXState(canonical) {
|
|
8904
|
+
switch (canonical) {
|
|
8905
|
+
case VexStatus.NotAffected: return "not_affected";
|
|
8906
|
+
case VexStatus.Fixed: return "resolved";
|
|
8907
|
+
case VexStatus.Affected: return "exploitable";
|
|
8908
|
+
/* c8 ignore next 2 — defensive: every VexStatus has a case above */
|
|
8909
|
+
default: return canonical;
|
|
8910
|
+
}
|
|
8911
|
+
}
|
|
8912
|
+
function componentFor(pid, pkg) {
|
|
8913
|
+
const c = {
|
|
8914
|
+
type: "application",
|
|
8915
|
+
name: pkg?.name ?? pid,
|
|
8916
|
+
"bom-ref": pid
|
|
8917
|
+
};
|
|
8918
|
+
if (pkg?.version) c.version = pkg.version;
|
|
8919
|
+
if (pkg?.purl ?? (pid.startsWith("pkg:") ? pid : void 0)) c.purl = pkg?.purl ?? pid;
|
|
8920
|
+
if (pkg?.cpe ?? (pid.startsWith("cpe:2.3:") ? pid : void 0)) c.cpe = pkg?.cpe ?? pid;
|
|
8921
|
+
return c;
|
|
8922
|
+
}
|
|
8923
|
+
function productIDsFor(o) {
|
|
8924
|
+
if (o.affectedPackages && o.affectedPackages.length > 0) {
|
|
8925
|
+
const ids = o.affectedPackages.map((p) => affectedPackageToIdentifier(p)).filter((id) => Boolean(id));
|
|
8926
|
+
if (ids.length > 0) return ids;
|
|
8927
|
+
}
|
|
8928
|
+
if (o.componentRef) return [o.componentRef];
|
|
8929
|
+
const m = PRODUCTS_LINE.exec(o.reason ?? "");
|
|
8930
|
+
if (m && m[1]) {
|
|
8931
|
+
const parts = m[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
8932
|
+
if (parts.length > 0) return parts;
|
|
8933
|
+
}
|
|
8934
|
+
return [DEFAULT_PRODUCT_ID];
|
|
8935
|
+
}
|
|
8936
|
+
function stripReasonAnnotations(reason) {
|
|
8937
|
+
return reason.replace(PRODUCTS_LINE, "").replace(RAW_JUST_LINE, "").replace(RESPONSE_LINE, "").trim();
|
|
8938
|
+
}
|
|
8939
|
+
function allMilestonesCompleted(o) {
|
|
8940
|
+
if (!o.milestones || o.milestones.length === 0) return false;
|
|
8941
|
+
return o.milestones.every((m) => m.status === MilestoneStatus.Completed);
|
|
8942
|
+
}
|
|
8943
|
+
function buildMetadata(a, docTime, converterVersion) {
|
|
8944
|
+
const metadata = {
|
|
8945
|
+
timestamp: docTime.toISOString().replace(/\.\d+Z$/, "Z"),
|
|
8946
|
+
tools: [{
|
|
8947
|
+
vendor: "mitre",
|
|
8948
|
+
name: "hdf-to-cyclonedx-vex",
|
|
8949
|
+
version: converterVersion
|
|
8950
|
+
}]
|
|
8951
|
+
};
|
|
8952
|
+
if (a.appliedBy?.identifier) {
|
|
8953
|
+
const author = {};
|
|
8954
|
+
if (a.appliedBy.type === IdentityType.Email) author.email = a.appliedBy.identifier;
|
|
8955
|
+
else author.name = a.appliedBy.identifier;
|
|
8956
|
+
metadata.authors = [author];
|
|
8957
|
+
}
|
|
8958
|
+
return metadata;
|
|
8959
|
+
}
|
|
8960
|
+
function buildSerialNumberSync(input, a) {
|
|
8961
|
+
if (a.amendmentId) return `urn:uuid:${a.amendmentId}`;
|
|
8962
|
+
return `urn:uuid:${fnv1a(input)}`;
|
|
8963
|
+
}
|
|
8964
|
+
function fnv1a(s) {
|
|
8965
|
+
let h = 2166136261;
|
|
8966
|
+
for (let i = 0; i < s.length; i++) {
|
|
8967
|
+
h ^= s.charCodeAt(i);
|
|
8968
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
8969
|
+
}
|
|
8970
|
+
const d = h.toString(16).padStart(8, "0");
|
|
8971
|
+
return (d + d + d + d).slice(0, 32);
|
|
8972
|
+
}
|
|
8973
|
+
//#endregion
|
|
5988
8974
|
//#region converters/oscal-to-hdf/typescript/detect.ts
|
|
5989
8975
|
/**
|
|
5990
8976
|
* OSCAL document type detection.
|
|
@@ -6073,7 +9059,7 @@ async function catalogToBaseline(catalog, rawInput) {
|
|
|
6073
9059
|
requirements,
|
|
6074
9060
|
groups,
|
|
6075
9061
|
generator: {
|
|
6076
|
-
name: "hdf
|
|
9062
|
+
name: "oscal-catalog-to-hdf",
|
|
6077
9063
|
version: "1.0.0"
|
|
6078
9064
|
}
|
|
6079
9065
|
};
|
|
@@ -6083,13 +9069,17 @@ function controlToBaselineRequirement(ctrl) {
|
|
|
6083
9069
|
const nistTag = controlIdToNistTag(ctrl.id);
|
|
6084
9070
|
const descriptions = buildCatalogDescriptions(ctrl);
|
|
6085
9071
|
const tags = buildCatalogTags(ctrl);
|
|
6086
|
-
|
|
9072
|
+
const req = {
|
|
6087
9073
|
id: nistTag,
|
|
6088
9074
|
title: ctrl.title,
|
|
6089
9075
|
impact: .5,
|
|
6090
9076
|
descriptions,
|
|
6091
9077
|
tags
|
|
6092
9078
|
};
|
|
9079
|
+
const controlType = deriveControlTypeFromTags([nistTag]);
|
|
9080
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
9081
|
+
if (extractPropValue(ctrl.props, "CORE") === "true") req.applicability = Applicability.Required;
|
|
9082
|
+
return req;
|
|
6093
9083
|
}
|
|
6094
9084
|
/** Creates HDF Description entries from control parts. */
|
|
6095
9085
|
function buildCatalogDescriptions(ctrl) {
|
|
@@ -6312,7 +9302,7 @@ async function convertOscalComponentToHdf(input) {
|
|
|
6312
9302
|
integrity,
|
|
6313
9303
|
requirements,
|
|
6314
9304
|
generator: {
|
|
6315
|
-
name: "hdf
|
|
9305
|
+
name: "oscal-component-to-hdf",
|
|
6316
9306
|
version: "1.0.0"
|
|
6317
9307
|
}
|
|
6318
9308
|
};
|
|
@@ -6373,7 +9363,7 @@ async function convertOscalSspToHdf(input) {
|
|
|
6373
9363
|
integrity,
|
|
6374
9364
|
components: [],
|
|
6375
9365
|
generator: {
|
|
6376
|
-
name: "hdf
|
|
9366
|
+
name: "oscal-ssp-to-hdf",
|
|
6377
9367
|
version: "1.0.0"
|
|
6378
9368
|
}
|
|
6379
9369
|
};
|
|
@@ -6500,13 +9490,13 @@ function sspComponentToHDFComponent(sc, componentControls) {
|
|
|
6500
9490
|
function mapOSCALComponentType(oscalType) {
|
|
6501
9491
|
switch (oscalType.toLowerCase()) {
|
|
6502
9492
|
case "software":
|
|
6503
|
-
case "this-system": return
|
|
6504
|
-
case "service": return
|
|
6505
|
-
case "hardware": return
|
|
6506
|
-
case "network": return
|
|
6507
|
-
case "database": return
|
|
6508
|
-
case "storage": return
|
|
6509
|
-
default: return
|
|
9493
|
+
case "this-system": return TargetType.Application;
|
|
9494
|
+
case "service": return TargetType.Application;
|
|
9495
|
+
case "hardware": return TargetType.Host;
|
|
9496
|
+
case "network": return TargetType.Network;
|
|
9497
|
+
case "database": return TargetType.Database;
|
|
9498
|
+
case "storage": return TargetType.Artifact;
|
|
9499
|
+
default: return TargetType.Application;
|
|
6510
9500
|
}
|
|
6511
9501
|
}
|
|
6512
9502
|
//#endregion
|
|
@@ -6543,7 +9533,7 @@ async function convertOscalSapToHdf(input) {
|
|
|
6543
9533
|
type: planType,
|
|
6544
9534
|
description,
|
|
6545
9535
|
generator: {
|
|
6546
|
-
name: "hdf
|
|
9536
|
+
name: "oscal-sap-to-hdf",
|
|
6547
9537
|
version: "1.0.0"
|
|
6548
9538
|
}
|
|
6549
9539
|
};
|
|
@@ -6673,7 +9663,7 @@ async function convertOscalPoamToHdf(input) {
|
|
|
6673
9663
|
version: meta.version,
|
|
6674
9664
|
appliedBy,
|
|
6675
9665
|
generator: {
|
|
6676
|
-
name: "hdf
|
|
9666
|
+
name: "oscal-poam-to-hdf",
|
|
6677
9667
|
version: "1.0.0"
|
|
6678
9668
|
}
|
|
6679
9669
|
};
|
|
@@ -6870,7 +9860,10 @@ function findingsToEvaluatedRequirement(controlId, findings, obsMap, riskMap, re
|
|
|
6870
9860
|
const descriptions = sarBuildDescriptions(findings, obsMap);
|
|
6871
9861
|
const results = [];
|
|
6872
9862
|
for (const f of findings) results.push(findingToRequirementResult(f, obsMap, riskMap, result));
|
|
6873
|
-
|
|
9863
|
+
const req = createRequirement(nistTag, title, descriptions, impact, results, { tags: { nist: [nistTag] } });
|
|
9864
|
+
const controlType = deriveControlTypeFromTags([nistTag]);
|
|
9865
|
+
if (controlType !== void 0) req.controlType = controlType;
|
|
9866
|
+
return req;
|
|
6874
9867
|
}
|
|
6875
9868
|
function findingToRequirementResult(f, obsMap, riskMap, result) {
|
|
6876
9869
|
const status = mapFindingStatus(f);
|
|
@@ -6983,6 +9976,6 @@ function sarBaselineName(result, sar) {
|
|
|
6983
9976
|
return toKebabCase(result.title || sar.metadata.title, "oscal-assessment-results");
|
|
6984
9977
|
}
|
|
6985
9978
|
//#endregion
|
|
6986
|
-
export { convertAwsConfigToHdf, convertBurpsuiteToHdf, convertConveyorToHdf, convertCyclonedxToHdf, convertDbprotectToHdf, convertDeptrackToHdf, convertFortifyToHdf, convertGitlabToHdf, convertGosecToHdf, convertGrypeToHdf, 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 };
|
|
9979
|
+
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 };
|
|
6987
9980
|
|
|
6988
9981
|
//# sourceMappingURL=index.js.map
|