@mitre/hdf-converters 3.6.0 → 3.7.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/detect.d.ts +5 -5
- package/dist/detect.d.ts.map +1 -1
- package/dist/detect.js +1 -1
- package/dist/index.d.ts +91 -79
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2188 -515
- package/dist/index.js.map +1 -1
- package/dist/{register-all-CDfYvwQF.js → register-all-BEuKQGEZ.js} +69 -64
- package/dist/register-all-BEuKQGEZ.js.map +1 -0
- package/dist/registry.d.ts +9 -10
- package/dist/registry.d.ts.map +1 -1
- package/package.json +7 -7
- package/dist/register-all-CDfYvwQF.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { n as detectFormat, r as detectConverter, t as registerAllFingerprints } from "./register-all-
|
|
1
|
+
import { n as detectFormat, o as isCheckovTestCaseName, r as detectConverter, t as registerAllFingerprints } from "./register-all-BEuKQGEZ.js";
|
|
2
2
|
import { flattenOverlays } from "@mitre/hdf-parsers";
|
|
3
|
-
import {
|
|
3
|
+
import { buildCsv, buildXml, canonicalJson, computeCvssScore, computeEffectiveStatus, cvssScoreToSeverity, encodeBase64Utf8, formatJsonNumber, formatTimestamp, formatTimestampSeconds, governingStatusOverride, impactToSeverity, isUnratedSeverity, normalizeHdfTimestamps, parseCsv, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, replaceHtmlTags, roundImpact, severityToImpact, severityToImpactWithAliases, sha256, stripHtml, stripHtml as stripHTML, trimUtcFraction, validateInputSize, worstStatus, xmlSafeText } from "@mitre/hdf-utilities";
|
|
4
4
|
import { Applicability, AuthorizationStatus, CVSSSeverity, CategorizationLevel, ControlType, Ecosystem, EvidenceType, HashAlgorithm, IdentityType, Justification, MilestoneStatus, OverrideType, PlanType, ResultStatus, Severity, TargetType, VerificationMethodEnum, Version, affectedPackageSchema, amendmentsSchema, bomSchema, commonSchema, comparisonSchema, componentSchema, createDescription, createMinimalBaseline, createRequirement, createResult, cvssSchema, dataFlowSchema, epssSchema, eventsSchema, extensionsSchema, hdfAmendmentsSchema, hdfBaselineSchema, hdfComparisonSchema, hdfEvidencePackageSchema, hdfPlanSchema, hdfRequirementChangeEventSchema, hdfResultsSchema, hdfSystemSchema, kevSchema, parameterSchema, planSchema, platformSchema, resultSchema, runnerSchema, severityToImpact as severityToImpact$1, statisticsSchema, systemSchema, targetSchema } from "@mitre/hdf-schema";
|
|
5
|
-
import { DEFAULT_COMPONENT_MANAGEMENT_NIST_TAGS, DEFAULT_CONFIG_MANAGEMENT_NIST_TAGS, DEFAULT_REMEDIATION_NIST_TAGS, DEFAULT_STATIC_ANALYSIS_NIST_TAGS, DEFAULT_STATIC_ANALYSIS_NIST_TAGS as DEFAULT_STATIC_ANALYSIS_NIST_TAGS$1, awsConfigMappedRevisions, getAwsConfigNistControlByIdentifier, getAwsConfigNistControlByName, getAwsConfigNistControlsBySubstring, getCCINistMappings, getCurrentNistRevision, getCweNistControl, getHipcheckNistControls, getNessusNistControl, getNiktoNistControl, getOwaspNistControl, getScoutsuiteNistControl, isNistStrict, nistToCci } from "@mitre/hdf-mappings";
|
|
5
|
+
import { DEFAULT_COMPONENT_MANAGEMENT_NIST_TAGS, DEFAULT_CONFIG_MANAGEMENT_NIST_TAGS, DEFAULT_REMEDIATION_NIST_TAGS, DEFAULT_STATIC_ANALYSIS_NIST_TAGS, DEFAULT_STATIC_ANALYSIS_NIST_TAGS as DEFAULT_STATIC_ANALYSIS_NIST_TAGS$1, SUPPORTED_NIST_REVISIONS, awsConfigMappedRevisions, getAwsConfigNistControlByIdentifier, getAwsConfigNistControlByName, getAwsConfigNistControlsBySubstring, getCCINistMappings, getCheckovCciNistMapping, getCurrentNistRevision, getCweNistControl, getHipcheckNistControls, getNessusNistControl, getNiktoNistControl, getOwaspNistControl, getScoutsuiteNistControl, isNistStrict, nistExists, nistToCci, normalizeNistId } from "@mitre/hdf-mappings";
|
|
6
6
|
import Ajv from "ajv";
|
|
7
7
|
import addFormats from "ajv-formats";
|
|
8
8
|
//#region ../hdf-validators/dist/index.js
|
|
@@ -250,6 +250,13 @@ function limitArray(items, maxItems = DEFAULT_MAX_ITEMS) {
|
|
|
250
250
|
};
|
|
251
251
|
}
|
|
252
252
|
/**
|
|
253
|
+
* Emit a non-fatal converter warning as "WARNING: <message>" on stderr, the
|
|
254
|
+
* TypeScript counterpart of Go's log.Printf("WARNING: ...").
|
|
255
|
+
*/
|
|
256
|
+
function emitConverterWarning(message) {
|
|
257
|
+
console.warn(`WARNING: ${message}`);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
253
260
|
* Limit an array and log a warning if truncated.
|
|
254
261
|
*
|
|
255
262
|
* Wraps {@link limitArray} with a console.warn call when items are truncated.
|
|
@@ -261,7 +268,7 @@ function limitArray(items, maxItems = DEFAULT_MAX_ITEMS) {
|
|
|
261
268
|
*/
|
|
262
269
|
function limitArrayWithWarning(items, label, maxItems = DEFAULT_MAX_ITEMS) {
|
|
263
270
|
const { items: limited, truncated } = limitArray(items, maxItems);
|
|
264
|
-
if (truncated)
|
|
271
|
+
if (truncated) emitConverterWarning(`Input truncated at ${limited.length} ${label} items (original: ${items.length})`);
|
|
265
272
|
return limited;
|
|
266
273
|
}
|
|
267
274
|
/**
|
|
@@ -323,10 +330,9 @@ function firstNonEmpty(...candidates) {
|
|
|
323
330
|
* converter name prefixed onto the error. A non-positive maxSize means "use
|
|
324
331
|
* the default" (an explicitly-passed 0 never rejects all non-empty input).
|
|
325
332
|
*/
|
|
326
|
-
function validateInputSize$1(input, converterName, maxSize =
|
|
327
|
-
const limit = maxSize > 0 ? maxSize : DEFAULT_MAX_INPUT_SIZE;
|
|
333
|
+
function validateInputSize$1(input, converterName, maxSize = 0) {
|
|
328
334
|
try {
|
|
329
|
-
validateInputSize(input,
|
|
335
|
+
validateInputSize(input, maxSize);
|
|
330
336
|
} catch (err) {
|
|
331
337
|
throw new Error(`${converterName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
332
338
|
}
|
|
@@ -747,6 +753,24 @@ function typeViolations(result) {
|
|
|
747
753
|
* - Results: snake_case → camelCase for all fields
|
|
748
754
|
* - Overlay flattening: merge overlay/wrapper baselines so every requirement has results
|
|
749
755
|
*/
|
|
756
|
+
/** Reserved passthrough key for the components round-trip carrier. */
|
|
757
|
+
const RESERVED_COMPONENTS_KEY = "hdf_components";
|
|
758
|
+
/**
|
|
759
|
+
* Copy provenance into a null-prototype object, keeping only own enumerable keys
|
|
760
|
+
* and dropping the reserved carrier key. A null prototype makes `__proto__`/
|
|
761
|
+
* `constructor` entries plain data rather than a prototype-pollution vector, and
|
|
762
|
+
* only a plain (non-array) object is treated as provenance. Returns undefined
|
|
763
|
+
* when there is nothing to carry.
|
|
764
|
+
*/
|
|
765
|
+
function sanitizeProvenance(raw) {
|
|
766
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return;
|
|
767
|
+
const out = Object.create(null);
|
|
768
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
769
|
+
if (key === RESERVED_COMPONENTS_KEY) continue;
|
|
770
|
+
out[key] = value;
|
|
771
|
+
}
|
|
772
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
773
|
+
}
|
|
750
774
|
/**
|
|
751
775
|
* Convert tags.severity string to a valid severity value.
|
|
752
776
|
* Returns null if the value is not a recognized severity.
|
|
@@ -813,7 +837,7 @@ function convertResult(v1Result) {
|
|
|
813
837
|
if (v1Result.exception !== void 0) v2Result.exception = v1Result.exception;
|
|
814
838
|
if (v1Result.backtrace !== void 0) v2Result.backtrace = v1Result.backtrace;
|
|
815
839
|
if (v1Result.resource_class !== void 0) v2Result.resource = v1Result.resource_class;
|
|
816
|
-
if (v1Result.resource_id !== void 0) v2Result.resourceId = v1Result.resource_id;
|
|
840
|
+
if (v1Result.resource_id !== void 0 && v1Result.resource_id !== null) v2Result.resourceId = typeof v1Result.resource_id === "string" ? v1Result.resource_id : canonicalJson(v1Result.resource_id);
|
|
817
841
|
return v2Result;
|
|
818
842
|
}
|
|
819
843
|
/**
|
|
@@ -879,7 +903,12 @@ function convertControl(v1Control) {
|
|
|
879
903
|
impact: v1Control.impact
|
|
880
904
|
};
|
|
881
905
|
if (v1Control.title !== void 0) v2Req.title = v1Control.title;
|
|
882
|
-
|
|
906
|
+
const descriptions = [...v1Control.descriptions ?? []];
|
|
907
|
+
if (!descriptions.some((d) => d.label === "default")) descriptions.unshift({
|
|
908
|
+
label: "default",
|
|
909
|
+
data: v1Control.desc ?? ""
|
|
910
|
+
});
|
|
911
|
+
v2Req.descriptions = descriptions;
|
|
883
912
|
if (v1Control.tags !== void 0) v2Req.tags = v1Control.tags;
|
|
884
913
|
if (v1Control.code !== void 0) v2Req.code = v1Control.code;
|
|
885
914
|
if (Array.isArray(v1Control.refs)) {
|
|
@@ -1087,6 +1116,7 @@ function convertLegacyHdf(v1Data, converterVersion = "1.0.0") {
|
|
|
1087
1116
|
}
|
|
1088
1117
|
v2.components = [component];
|
|
1089
1118
|
}
|
|
1119
|
+
if (Array.isArray(v1Data.passthrough?.hdf_components) && v1Data.passthrough.hdf_components.length > 0) v2.components = v1Data.passthrough.hdf_components;
|
|
1090
1120
|
v2.generator = v1Data.generator ?? {
|
|
1091
1121
|
name: "legacyhdf-to-hdf",
|
|
1092
1122
|
version: converterVersion
|
|
@@ -1094,13 +1124,15 @@ function convertLegacyHdf(v1Data, converterVersion = "1.0.0") {
|
|
|
1094
1124
|
v2.tool = toolIdentity(v1Data.version);
|
|
1095
1125
|
const timestamp = documentTimestamp(v1Data);
|
|
1096
1126
|
if (timestamp) v2.timestamp = timestamp;
|
|
1127
|
+
const carriedProvenance = sanitizeProvenance(v1Data.passthrough);
|
|
1097
1128
|
const knownV1Fields = /* @__PURE__ */ new Set([
|
|
1098
1129
|
"version",
|
|
1099
1130
|
"platform",
|
|
1100
1131
|
"profiles",
|
|
1101
1132
|
"statistics",
|
|
1102
1133
|
"generator",
|
|
1103
|
-
"timestamp"
|
|
1134
|
+
"timestamp",
|
|
1135
|
+
"passthrough"
|
|
1104
1136
|
]);
|
|
1105
1137
|
const extensionFields = {};
|
|
1106
1138
|
for (const [key, value] of Object.entries(v1Data)) if (!knownV1Fields.has(key)) extensionFields[key] = value;
|
|
@@ -1108,6 +1140,10 @@ function convertLegacyHdf(v1Data, converterVersion = "1.0.0") {
|
|
|
1108
1140
|
...extensionFields,
|
|
1109
1141
|
v1_version: v1Data.version
|
|
1110
1142
|
};
|
|
1143
|
+
if (carriedProvenance) v2.extensions = {
|
|
1144
|
+
...v2.extensions ?? {},
|
|
1145
|
+
passthrough: carriedProvenance
|
|
1146
|
+
};
|
|
1111
1147
|
return flattenOverlays(v2).results;
|
|
1112
1148
|
}
|
|
1113
1149
|
/**
|
|
@@ -1537,7 +1573,8 @@ function buildBaseline$1(name, findings, resultsChecksum) {
|
|
|
1537
1573
|
groups.set(id, [f]);
|
|
1538
1574
|
}
|
|
1539
1575
|
}
|
|
1540
|
-
|
|
1576
|
+
const requirements = order.map((id) => buildRequirement$19(id, groups.get(id)));
|
|
1577
|
+
return createMinimalBaseline(name, requirements, { resultsChecksum });
|
|
1541
1578
|
}
|
|
1542
1579
|
/** Converts an ASFF document to HDF Results JSON. */
|
|
1543
1580
|
async function convertAsffToHdf(input, converterVersion = "1.0.0") {
|
|
@@ -1983,14 +2020,16 @@ async function convertJunitToHdf(input, converterVersion = "1.0.0") {
|
|
|
1983
2020
|
if (!input || !input.trim()) throw new Error("Empty input");
|
|
1984
2021
|
validateInputSize$1(input, "junit");
|
|
1985
2022
|
const { suites, name } = parseJUnitXML(input);
|
|
2023
|
+
warnIfCheckov(suites);
|
|
1986
2024
|
const scanTime = resolveScanTime(suites);
|
|
1987
2025
|
const requirements = buildRequirements(suites, scanTime);
|
|
1988
2026
|
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("junit-no-findings", `JUnit scanned ${noFindingsTarget(name, suites)} and reported zero findings.`, scanTime));
|
|
2027
|
+
const resultsChecksum = await inputChecksum(input);
|
|
1989
2028
|
return buildHdfResults({
|
|
1990
2029
|
generatorName: "junit-to-hdf",
|
|
1991
2030
|
converterVersion,
|
|
1992
2031
|
toolName: "JUnit XML",
|
|
1993
|
-
baselines: [createMinimalBaseline(name, requirements, { resultsChecksum
|
|
2032
|
+
baselines: [createMinimalBaseline(name, requirements, { resultsChecksum })],
|
|
1994
2033
|
components: [{
|
|
1995
2034
|
type: TargetType.Application,
|
|
1996
2035
|
name
|
|
@@ -1998,6 +2037,10 @@ async function convertJunitToHdf(input, converterVersion = "1.0.0") {
|
|
|
1998
2037
|
timestamp: scanTime
|
|
1999
2038
|
});
|
|
2000
2039
|
}
|
|
2040
|
+
const CHECKOV_JUNIT_WARNING = "input looks like Checkov JUnit XML. junit-to-hdf keeps only Checkov's display strings and drops the check_id, severity and guideline that control mappings key on; re-run Checkov with -o json and convert that output with checkov-to-hdf instead.";
|
|
2041
|
+
function warnIfCheckov(suites) {
|
|
2042
|
+
if (suites.some((suite) => (suite.testcase ?? []).some((tc) => isCheckovTestCaseName(String(tc["@_name"] ?? ""))))) emitConverterWarning(CHECKOV_JUNIT_WARNING);
|
|
2043
|
+
}
|
|
2001
2044
|
function hostComponents(suites) {
|
|
2002
2045
|
const hosts = [];
|
|
2003
2046
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -2020,10 +2063,19 @@ function resolveScanTime(suites) {
|
|
|
2020
2063
|
}
|
|
2021
2064
|
return /* @__PURE__ */ new Date();
|
|
2022
2065
|
}
|
|
2066
|
+
function flattenSuites(suites) {
|
|
2067
|
+
const out = [];
|
|
2068
|
+
for (const suite of suites) {
|
|
2069
|
+
const { testsuite: nested, ...rest } = suite;
|
|
2070
|
+
out.push(rest);
|
|
2071
|
+
if (nested) out.push(...flattenSuites(nested));
|
|
2072
|
+
}
|
|
2073
|
+
return out;
|
|
2074
|
+
}
|
|
2023
2075
|
function parseJUnitXML(input) {
|
|
2024
2076
|
const parsed = parseXmlWithArrays(input, ARRAY_TAGS$2, { attributeNamePrefix: "@_" });
|
|
2025
2077
|
if (parsed.testsuites) {
|
|
2026
|
-
const suites =
|
|
2078
|
+
const suites = flattenSuites(parsed.testsuites.testsuite ?? []);
|
|
2027
2079
|
const name = parsed.testsuites["@_name"] ? decodeXmlEntities$6(parsed.testsuites["@_name"]) : "JUnit Test Results";
|
|
2028
2080
|
const looseCases = parsed.testsuites.testcase ?? [];
|
|
2029
2081
|
if (looseCases.length > 0) suites.push({
|
|
@@ -2037,11 +2089,12 @@ function parseJUnitXML(input) {
|
|
|
2037
2089
|
}
|
|
2038
2090
|
if (parsed.testsuite) {
|
|
2039
2091
|
const suite = parsed.testsuite;
|
|
2040
|
-
const
|
|
2041
|
-
const suiteName =
|
|
2092
|
+
const rootSuites = Array.isArray(suite) ? suite : [suite];
|
|
2093
|
+
const suiteName = rootSuites[0]?.["@_name"];
|
|
2094
|
+
const name = suiteName ? decodeXmlEntities$6(suiteName) : "JUnit Test Results";
|
|
2042
2095
|
return {
|
|
2043
|
-
suites,
|
|
2044
|
-
name
|
|
2096
|
+
suites: flattenSuites(rootSuites),
|
|
2097
|
+
name
|
|
2045
2098
|
};
|
|
2046
2099
|
}
|
|
2047
2100
|
throw new Error("Input is not a JUnit XML document: expected <testsuites> or <testsuite> root element");
|
|
@@ -2949,7 +3002,11 @@ function cklVulnToModel(v) {
|
|
|
2949
3002
|
}
|
|
2950
3003
|
/** Serialize the Checklist model to CKL XML (with declaration). */
|
|
2951
3004
|
function serializeCkl(cl) {
|
|
2952
|
-
|
|
3005
|
+
if (cl.stigs.length === 0) throw new Error("serialize ckl: checklist has no stigs");
|
|
3006
|
+
cl.stigs.forEach((s, i) => {
|
|
3007
|
+
if (s.vulns.length === 0) throw new Error(`serialize ckl: stig ${i + 1} has no rules`);
|
|
3008
|
+
});
|
|
3009
|
+
const xmlObj = { CHECKLIST: {
|
|
2953
3010
|
ASSET: {
|
|
2954
3011
|
ROLE: cl.asset.role || "None",
|
|
2955
3012
|
ASSET_TYPE: cl.asset.assetType || "Computing",
|
|
@@ -2968,7 +3025,8 @@ function serializeCkl(cl) {
|
|
|
2968
3025
|
STIG_INFO: { SI_DATA: stigInfoSiData(s) },
|
|
2969
3026
|
VULN: s.vulns.map(modelVulnToCkl)
|
|
2970
3027
|
})) }
|
|
2971
|
-
} }
|
|
3028
|
+
} };
|
|
3029
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n${buildXml(xmlObj, BUILD_OPTIONS$1)}`;
|
|
2972
3030
|
}
|
|
2973
3031
|
function stigInfoSiData(s) {
|
|
2974
3032
|
return [
|
|
@@ -3121,6 +3179,10 @@ function cklbRuleToModel(r) {
|
|
|
3121
3179
|
}
|
|
3122
3180
|
/** Serialize the Checklist model to CKLB JSON. */
|
|
3123
3181
|
function serializeCklb(cl) {
|
|
3182
|
+
if (cl.stigs.length === 0) throw new Error("serialize cklb: checklist has no stigs");
|
|
3183
|
+
cl.stigs.forEach((s, i) => {
|
|
3184
|
+
if (s.vulns.length === 0) throw new Error(`serialize cklb: stig ${i + 1} has no rules`);
|
|
3185
|
+
});
|
|
3124
3186
|
const doc = {
|
|
3125
3187
|
title: cklbTitle(cl),
|
|
3126
3188
|
cklb_version: cl.cklbVersion || "1.0",
|
|
@@ -3150,7 +3212,7 @@ function serializeCklb(cl) {
|
|
|
3150
3212
|
uuid: s.uuid ?? "",
|
|
3151
3213
|
...s.referenceIdentifier ? { reference_identifier: s.referenceIdentifier } : {},
|
|
3152
3214
|
rules: s.vulns.map((v) => ({
|
|
3153
|
-
group_id: v.groupID || v.vulnNum,
|
|
3215
|
+
group_id: v.groupID || v.vulnNum || "",
|
|
3154
3216
|
group_title: v.groupTitle ?? "",
|
|
3155
3217
|
rule_id: v.ruleID ?? "",
|
|
3156
3218
|
rule_version: v.ruleVer ?? "",
|
|
@@ -3208,7 +3270,8 @@ function checklistToHdf(cl, resultsChecksum, generatorName) {
|
|
|
3208
3270
|
return hdf;
|
|
3209
3271
|
}
|
|
3210
3272
|
function stigToBaseline(s, resultsChecksum, scanTime) {
|
|
3211
|
-
const
|
|
3273
|
+
const requirements = s.vulns.map((v) => vulnToRequirement(v, scanTime));
|
|
3274
|
+
const baseline = createMinimalBaseline("STIG Checklist Scan", requirements, { resultsChecksum });
|
|
3212
3275
|
if (s.title) baseline.title = s.title;
|
|
3213
3276
|
if (s.version) baseline.version = s.version;
|
|
3214
3277
|
const ext = baselineExtensions(s);
|
|
@@ -3344,6 +3407,9 @@ function requirementEffectiveStatus(req) {
|
|
|
3344
3407
|
function hdfToChecklist(input) {
|
|
3345
3408
|
const hdf = requireHdfResults(input, "hdf to checklist").doc;
|
|
3346
3409
|
if (hdf.baselines.length === 0) throw new Error("hdf to checklist: HDF has no baselines");
|
|
3410
|
+
hdf.baselines.forEach((bl, i) => {
|
|
3411
|
+
if (!bl || !Array.isArray(bl.requirements) || bl.requirements.length === 0) throw new Error(`hdf to checklist: baseline ${i + 1} has no requirements`);
|
|
3412
|
+
});
|
|
3347
3413
|
const ext = hdf.extensions ?? {};
|
|
3348
3414
|
const format = strVal(ext, "checklistFormat") || "ckl";
|
|
3349
3415
|
const cklbVersion = strVal(ext, "cklbVersion");
|
|
@@ -3470,7 +3536,7 @@ function formatOverride(o) {
|
|
|
3470
3536
|
}
|
|
3471
3537
|
function overrideSeverity(req) {
|
|
3472
3538
|
for (const o of req.statusOverrides ?? []) if (o.impact) return {
|
|
3473
|
-
severity:
|
|
3539
|
+
severity: cklSeverityOrFloor(o.impact.value),
|
|
3474
3540
|
justification: o.reason ?? ""
|
|
3475
3541
|
};
|
|
3476
3542
|
return {
|
|
@@ -3487,11 +3553,14 @@ function cklSeverityFromImpact(impact) {
|
|
|
3487
3553
|
default: return "";
|
|
3488
3554
|
}
|
|
3489
3555
|
}
|
|
3556
|
+
function cklSeverityOrFloor(impact) {
|
|
3557
|
+
return cklSeverityFromImpact(impact) || "low";
|
|
3558
|
+
}
|
|
3490
3559
|
function resolveSeverity(req, tags) {
|
|
3491
3560
|
const tagSev = strVal(tags, "severity");
|
|
3492
3561
|
if (tagSev) return tagSev;
|
|
3493
3562
|
if (req.severity) return String(req.severity).toLowerCase();
|
|
3494
|
-
return
|
|
3563
|
+
return cklSeverityOrFloor(req.impact ?? 0);
|
|
3495
3564
|
}
|
|
3496
3565
|
function resolveCcis(tags) {
|
|
3497
3566
|
const direct = strSlice(tags, "cci");
|
|
@@ -4120,9 +4189,10 @@ async function convertSemgrepToHdf(input, converterVersion = "1.0.0") {
|
|
|
4120
4189
|
}
|
|
4121
4190
|
if (report.errors.length > 0) requirements.push(buildErrorsRequirement(report.errors, startTime));
|
|
4122
4191
|
requirements.push(buildCoverageRequirement(report, groups.size, startTime));
|
|
4192
|
+
const resultsChecksum = await inputChecksum(input);
|
|
4123
4193
|
const baseline = createMinimalBaseline("Semgrep Scan", requirements, {
|
|
4124
4194
|
title: "Semgrep static analysis scan",
|
|
4125
|
-
resultsChecksum
|
|
4195
|
+
resultsChecksum
|
|
4126
4196
|
});
|
|
4127
4197
|
return buildHdfResults({
|
|
4128
4198
|
generatorName: "semgrep-to-hdf",
|
|
@@ -4363,9 +4433,10 @@ function convertSingleProject(report, resultsChecksum, scanTime) {
|
|
|
4363
4433
|
const target = report.projectName ?? report.path ?? "project";
|
|
4364
4434
|
requirements.push(buildNoFindingsRequirement("snyk-no-findings", `Snyk scanned ${target} and reported zero vulnerable components.`, scanTime));
|
|
4365
4435
|
}
|
|
4436
|
+
const title = `Snyk Project: ${report.projectName ?? ""} Snyk Path: ${report.path ?? ""}`;
|
|
4366
4437
|
return createMinimalBaseline("Snyk Scan", requirements, {
|
|
4367
4438
|
resultsChecksum,
|
|
4368
|
-
title
|
|
4439
|
+
title,
|
|
4369
4440
|
summary: report.summary
|
|
4370
4441
|
});
|
|
4371
4442
|
}
|
|
@@ -4558,7 +4629,8 @@ function convertMatchToRequirement(match, isIgnored, targetName, startTime) {
|
|
|
4558
4629
|
message,
|
|
4559
4630
|
startTime
|
|
4560
4631
|
};
|
|
4561
|
-
const
|
|
4632
|
+
const cciTags = nistToCci(DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
|
|
4633
|
+
const tags = buildNistCciTags(DEFAULT_STATIC_ANALYSIS_NIST_TAGS, cciTags);
|
|
4562
4634
|
markUnratedSeverity(tags, severity);
|
|
4563
4635
|
const requirement = {
|
|
4564
4636
|
id: isIgnored ? `Grype-Ignored-Match/${cveId}` : `Grype/${cveId}`,
|
|
@@ -4597,8 +4669,13 @@ function convertMatchToRequirement(match, isIgnored, targetName, startTime) {
|
|
|
4597
4669
|
if (kev) requirement.kev = kev;
|
|
4598
4670
|
return requirement;
|
|
4599
4671
|
}
|
|
4672
|
+
function grypeTargetName(target) {
|
|
4673
|
+
if (typeof target === "string") return target || "Grype Scan";
|
|
4674
|
+
return target?.userInput || "Grype Scan";
|
|
4675
|
+
}
|
|
4600
4676
|
function buildComponent$2(report, targetName) {
|
|
4601
|
-
const
|
|
4677
|
+
const rawTarget = report.source?.target;
|
|
4678
|
+
const t = typeof rawTarget === "object" ? rawTarget : void 0;
|
|
4602
4679
|
const firstRepoDigest = t?.repoDigests?.find((d) => d);
|
|
4603
4680
|
const firstTag = t?.tags?.find((tag) => tag);
|
|
4604
4681
|
const isImage = Boolean(t && (t.imageID || t.manifestDigest || firstRepoDigest || firstTag));
|
|
@@ -4625,7 +4702,7 @@ async function convertGrypeToHdf(input, converterVersion = "1.0.0") {
|
|
|
4625
4702
|
const resultsChecksum = await inputChecksum(input);
|
|
4626
4703
|
const grypeData = parseJSON(input);
|
|
4627
4704
|
const requirements = [];
|
|
4628
|
-
const targetName = grypeData.source?.target
|
|
4705
|
+
const targetName = grypeTargetName(grypeData.source?.target);
|
|
4629
4706
|
const scanTime = grypeData.descriptor?.timestamp ? parseTimestamp(grypeData.descriptor.timestamp) : null;
|
|
4630
4707
|
const resultStart = scanTime ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
|
|
4631
4708
|
if (grypeData.matches && grypeData.matches.length > 0) {
|
|
@@ -5680,10 +5757,13 @@ function buildResult(r) {
|
|
|
5680
5757
|
const q = r.EvaluationResultIdentifier.EvaluationResultQualifier;
|
|
5681
5758
|
const status = mapComplianceStatus(r.ComplianceType);
|
|
5682
5759
|
const codeDesc = buildCodeDesc$8(q);
|
|
5683
|
-
|
|
5760
|
+
const message = buildResultMessage(codeDesc, r.Annotation, status);
|
|
5761
|
+
const startTime = (r.ConfigRuleInvokedTime ? parseTimestamp(r.ConfigRuleInvokedTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
|
|
5762
|
+
const runTime = computeRunTime$1(r.ConfigRuleInvokedTime, r.ResultRecordedTime);
|
|
5763
|
+
return createResult(status, message, {
|
|
5684
5764
|
codeDesc,
|
|
5685
|
-
startTime
|
|
5686
|
-
runTime
|
|
5765
|
+
startTime,
|
|
5766
|
+
runTime
|
|
5687
5767
|
});
|
|
5688
5768
|
}
|
|
5689
5769
|
/**
|
|
@@ -5747,8 +5827,9 @@ async function convertAwsConfigToHdf(input, converterVersion = "1.0.0") {
|
|
|
5747
5827
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
5748
5828
|
if (truncatedRules) console.warn(`WARNING: Input truncated at ${limitedRules.length} ConfigRule items (original: ${data.ConfigRules.length})`);
|
|
5749
5829
|
checkRevisionAlignment(limitedRules);
|
|
5830
|
+
const requirements = limitedRules.map(buildRequirement$15);
|
|
5750
5831
|
const baseline = {
|
|
5751
|
-
...createMinimalBaseline("AWS Config",
|
|
5832
|
+
...createMinimalBaseline("AWS Config", requirements, { resultsChecksum }),
|
|
5752
5833
|
title: "AWS Config Compliance Results",
|
|
5753
5834
|
version: "1.0.0",
|
|
5754
5835
|
maintainer: "Amazon Web Services"
|
|
@@ -5826,11 +5907,25 @@ function renderCodeBlock(codeBlock) {
|
|
|
5826
5907
|
/**
|
|
5827
5908
|
* Converts a group of checks sharing a check_id into one EvaluatedRequirement.
|
|
5828
5909
|
*/
|
|
5910
|
+
/**
|
|
5911
|
+
* Resolves a check's NIST controls and CCIs from the Checkov mapping dataset,
|
|
5912
|
+
* falling back to the static-analysis controls when unmapped. Mirrors Go's controlsFor.
|
|
5913
|
+
*/
|
|
5914
|
+
function controlsFor(checkId) {
|
|
5915
|
+
const mapping = getCheckovCciNistMapping(checkId);
|
|
5916
|
+
if (mapping && mapping.nist.length > 0) return mapping;
|
|
5917
|
+
const fallback = [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS];
|
|
5918
|
+
return {
|
|
5919
|
+
nist: fallback,
|
|
5920
|
+
cci: nistToCci(fallback)
|
|
5921
|
+
};
|
|
5922
|
+
}
|
|
5829
5923
|
function buildRequirement$14(checkId, group, scanTime) {
|
|
5830
5924
|
const checks = group.map((c) => c.check);
|
|
5831
5925
|
const rep = checks[0];
|
|
5832
5926
|
const impact = getImpact$8(rep.severity);
|
|
5833
|
-
const
|
|
5927
|
+
const { nist, cci } = controlsFor(checkId);
|
|
5928
|
+
const tags = buildNistCciTags(nist, cci);
|
|
5834
5929
|
const checkTypes = [...new Set(group.map((c) => c.checkType).filter((t) => t !== ""))].sort();
|
|
5835
5930
|
if (checkTypes.length > 0) tags["check_type"] = checkTypes;
|
|
5836
5931
|
if (rep.bc_check_id) tags["bc_check_id"] = rep.bc_check_id;
|
|
@@ -5853,7 +5948,7 @@ function buildRequirement$14(checkId, group, scanTime) {
|
|
|
5853
5948
|
if (Array.isArray(rep.file_line_range) && typeof rep.file_line_range[0] === "number") loc.line = rep.file_line_range[0];
|
|
5854
5949
|
req.sourceLocation = loc;
|
|
5855
5950
|
}
|
|
5856
|
-
const controlType = deriveControlTypeFromTags(
|
|
5951
|
+
const controlType = deriveControlTypeFromTags(nist);
|
|
5857
5952
|
if (controlType !== void 0) req.controlType = controlType;
|
|
5858
5953
|
return req;
|
|
5859
5954
|
}
|
|
@@ -6358,7 +6453,8 @@ async function convertZapToHdf(input, converterVersion = "1.0.0") {
|
|
|
6358
6453
|
requirements.push(buildNoFindingsRequirement("zap-no-findings", `OWASP ZAP scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
6359
6454
|
}
|
|
6360
6455
|
const baselineTitle = site["@name"] || site["@host"] ? `OWASP ZAP Scan of ${site["@name"] ?? targetName}` : "OWASP ZAP Scan";
|
|
6361
|
-
const
|
|
6456
|
+
const scanLabel = multiSite ? `OWASP ZAP Scan: ${siteLabel(site, i)}` : "OWASP ZAP Scan";
|
|
6457
|
+
const baseline = createMinimalBaseline(scanLabel, requirements, {
|
|
6362
6458
|
resultsChecksum,
|
|
6363
6459
|
title: baselineTitle,
|
|
6364
6460
|
summary
|
|
@@ -6967,8 +7063,9 @@ function statusOf(req) {
|
|
|
6967
7063
|
expiresAt: getStr(o, "expiresAt") || void 0
|
|
6968
7064
|
}];
|
|
6969
7065
|
});
|
|
7066
|
+
const impact = typeof req.impact === "number" ? req.impact : 1;
|
|
6970
7067
|
const rollup = computeEffectiveStatus({
|
|
6971
|
-
impact
|
|
7068
|
+
impact,
|
|
6972
7069
|
resultStatuses: resultStatuses(req),
|
|
6973
7070
|
overrides: overrideInputs
|
|
6974
7071
|
});
|
|
@@ -7083,11 +7180,17 @@ var RawNumber = class {
|
|
|
7083
7180
|
this.token = token;
|
|
7084
7181
|
}
|
|
7085
7182
|
};
|
|
7086
|
-
/**
|
|
7183
|
+
/**
|
|
7184
|
+
* Wrap a number as a RawNumber whose token always bears a decimal point.
|
|
7185
|
+
*
|
|
7186
|
+
* formatJsonNumber renders positionally at every magnitude, matching Go's
|
|
7187
|
+
* strconv 'f'. String() would switch to exponent notation outside
|
|
7188
|
+
* [1e-6, 1e21) and emit a token with no decimal point at all — defeating the
|
|
7189
|
+
* one thing this function exists to guarantee, and diverging from the Go peer.
|
|
7190
|
+
*/
|
|
7087
7191
|
function floatNumber(f) {
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
return new RawNumber(s);
|
|
7192
|
+
const s = Object.is(f, -0) ? "0" : formatJsonNumber(f);
|
|
7193
|
+
return new RawNumber(s.includes(".") ? s : s + ".0");
|
|
7091
7194
|
}
|
|
7092
7195
|
const RAWNUM_MARK = String.fromCharCode(1);
|
|
7093
7196
|
const RAWNUM_RE = /"\\u0001(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\\u0001"/g;
|
|
@@ -8235,13 +8338,15 @@ function buildFinding$1(req, baseline, docTimestamp, tool, generator, component,
|
|
|
8235
8338
|
if (device) finding.device = device;
|
|
8236
8339
|
setIf(finding, "raw_data", getStr(req, "code"));
|
|
8237
8340
|
setIf(finding, "message", firstResultMessage$1(req));
|
|
8238
|
-
const evidences = buildEvidences(req);
|
|
8239
|
-
if (evidences.length > 0) finding.evidences = evidences;
|
|
8240
8341
|
setIf(finding, "status_detail", st.rollup);
|
|
8241
|
-
const remediation = remediationText$1(req);
|
|
8242
|
-
if (remediation !== "") finding.remediation = { desc: remediation };
|
|
8243
8342
|
if (hasCVSS) finding.vulnerabilities = buildVulnerabilities(cvssList, req);
|
|
8244
|
-
else
|
|
8343
|
+
else {
|
|
8344
|
+
const evidences = buildEvidences(req);
|
|
8345
|
+
if (evidences.length > 0) finding.evidences = evidences;
|
|
8346
|
+
const remediation = remediationText$1(req);
|
|
8347
|
+
if (remediation !== "") finding.remediation = { desc: remediation };
|
|
8348
|
+
finding.compliance = buildCompliance(req, baseline, title, st.raw);
|
|
8349
|
+
}
|
|
8245
8350
|
return finding;
|
|
8246
8351
|
}
|
|
8247
8352
|
function severityID(req) {
|
|
@@ -8585,8 +8690,10 @@ function buildFinding(req, ctx) {
|
|
|
8585
8690
|
const rem = remediation(req);
|
|
8586
8691
|
if (rem) finding.Remediation = rem;
|
|
8587
8692
|
const refs = allRefURLs(req);
|
|
8588
|
-
if (refs.length > 0)
|
|
8589
|
-
|
|
8693
|
+
if (refs.length > 0) {
|
|
8694
|
+
if (vulns.length > 0) vulns[0].ReferenceUrls = refs;
|
|
8695
|
+
else finding.SourceUrl = refs[0];
|
|
8696
|
+
}
|
|
8590
8697
|
if (vulns.length > 0) finding.Vulnerabilities = vulns;
|
|
8591
8698
|
return finding;
|
|
8592
8699
|
}
|
|
@@ -8814,9 +8921,7 @@ async function convertSplunkToHdf(input, converterVersion = "1.0.0") {
|
|
|
8814
8921
|
case "profile":
|
|
8815
8922
|
profiles.push(event);
|
|
8816
8923
|
break;
|
|
8817
|
-
case "control":
|
|
8818
|
-
controls.push(event);
|
|
8819
|
-
break;
|
|
8924
|
+
case "control": controls.push(event);
|
|
8820
8925
|
}
|
|
8821
8926
|
if (headers.length !== 1) throw new Error(`Expected 1 header event, got ${headers.length}`);
|
|
8822
8927
|
const header = headers[0];
|
|
@@ -9235,9 +9340,10 @@ async function convertGitlabToHdf(input, converterVersion = "1.0.0") {
|
|
|
9235
9340
|
const ts = startTime ? parseTimestamp(startTime) ?? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date();
|
|
9236
9341
|
requirements.push(buildNoFindingsRequirement("gitlab-no-findings", `GitLab ${label} scan via ${scannerName} reported zero findings.`, ts));
|
|
9237
9342
|
}
|
|
9343
|
+
const baselineTitle = `GitLab ${label} Security Scan`;
|
|
9238
9344
|
const baseline = createMinimalBaseline("GitLab Security Scan", requirements, {
|
|
9239
9345
|
resultsChecksum,
|
|
9240
|
-
title:
|
|
9346
|
+
title: baselineTitle,
|
|
9241
9347
|
summary: `Scanner: ${scannerName}${scannerVersion ? ` v${scannerVersion}` : ""}`
|
|
9242
9348
|
});
|
|
9243
9349
|
const tool = { name: scannerName };
|
|
@@ -9728,10 +9834,11 @@ async function convertTrufflehogToHdf(input, converterVersion = "1.0.0") {
|
|
|
9728
9834
|
const target = findGitRepoURL(limitedFindings) ?? limitedFindings[0]?.SourceName ?? "the target source";
|
|
9729
9835
|
requirements.push(buildNoFindingsRequirement("trufflehog-no-findings", `TruffleHog scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
9730
9836
|
}
|
|
9837
|
+
const baselineTitle = `TruffleHog Scan (${limitedFindings[0]?.SourceName ?? "trufflehog"})`;
|
|
9731
9838
|
const hdf = {
|
|
9732
9839
|
baselines: [createMinimalBaseline("TruffleHog Scan", requirements, {
|
|
9733
9840
|
resultsChecksum,
|
|
9734
|
-
title:
|
|
9841
|
+
title: baselineTitle
|
|
9735
9842
|
})],
|
|
9736
9843
|
generator: {
|
|
9737
9844
|
name: "trufflehog-to-hdf",
|
|
@@ -9861,10 +9968,12 @@ async function convertHipcheckToHdf(input, converterVersion = "1.0.0") {
|
|
|
9861
9968
|
for (const e of report.errored ?? []) requirements.push(buildErroredRequirement(e, startTime));
|
|
9862
9969
|
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("hipcheck-no-findings", `Hipcheck analyzed ${repoIdent(report)} and reported zero analyses.`, startTime));
|
|
9863
9970
|
const summary = buildSummary$1(report.recommendation ?? {});
|
|
9971
|
+
const title = `Hipcheck analysis of ${repoIdent(report)} @ ${report.repo_head ?? ""}`;
|
|
9972
|
+
const resultsChecksum = await inputChecksum(input);
|
|
9864
9973
|
const baseline = createMinimalBaseline("Hipcheck Scan", requirements, {
|
|
9865
|
-
title
|
|
9974
|
+
title,
|
|
9866
9975
|
summary,
|
|
9867
|
-
resultsChecksum
|
|
9976
|
+
resultsChecksum
|
|
9868
9977
|
});
|
|
9869
9978
|
const ident = repoIdent(report);
|
|
9870
9979
|
return buildHdfResults({
|
|
@@ -9996,9 +10105,10 @@ async function convertBurpsuiteToHdf(input, converterVersion = "1.0.0") {
|
|
|
9996
10105
|
for (const [issueType, groupIssues] of groups) requirements.push(buildRequirement$11(issueType, groupIssues, resultStartTime));
|
|
9997
10106
|
const targetName = limitedIssues.length > 0 ? (limitedIssues[0].host?.["#text"] ?? "Unknown").trim() : "Unknown";
|
|
9998
10107
|
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("burpsuite-no-findings", `Burp Suite scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
10108
|
+
const title = `BurpSuite Scan: ${targetName}`;
|
|
9999
10109
|
const baseline = createMinimalBaseline("BurpSuite Scan", requirements, {
|
|
10000
10110
|
resultsChecksum,
|
|
10001
|
-
title
|
|
10111
|
+
title
|
|
10002
10112
|
});
|
|
10003
10113
|
const tool = { name: "BurpSuite" };
|
|
10004
10114
|
if (burpVersion) tool.version = burpVersion;
|
|
@@ -10174,9 +10284,11 @@ function buildScanTarget(f) {
|
|
|
10174
10284
|
const assetType = (f["Asset Type"] ?? "").trim();
|
|
10175
10285
|
const asset = (f["Asset"] ?? "").trim();
|
|
10176
10286
|
let name = instance;
|
|
10177
|
-
if (!name)
|
|
10178
|
-
|
|
10179
|
-
|
|
10287
|
+
if (!name) {
|
|
10288
|
+
if (ip && port) name = `${ip}:${port}`;
|
|
10289
|
+
else if (ip) name = ip;
|
|
10290
|
+
else name = asset;
|
|
10291
|
+
}
|
|
10180
10292
|
if (!name) return;
|
|
10181
10293
|
const comp = {
|
|
10182
10294
|
name,
|
|
@@ -10215,7 +10327,8 @@ function parseDate(dateStr) {
|
|
|
10215
10327
|
const trimmed = dateStr.trim();
|
|
10216
10328
|
if (!trimmed) return ZERO_DATE;
|
|
10217
10329
|
const month = /^([A-Z][a-z]{2}) (\d{1,2}) (\d{4}) (\d{2}:\d{2})$/.exec(trimmed);
|
|
10218
|
-
|
|
10330
|
+
const normalized = month ? `${month[3]}-${MONTH_ABBR[month[1]] ?? "00"}-${month[2].padStart(2, "0")} ${month[4]}` : trimmed;
|
|
10331
|
+
return parseTimestamp(normalized) ?? ZERO_DATE;
|
|
10219
10332
|
}
|
|
10220
10333
|
/**
|
|
10221
10334
|
* Derives the top-level HDF timestamp from the source, preferring the "Start
|
|
@@ -10256,7 +10369,8 @@ function buildRequirement$10(checkID, findings, hasStatus) {
|
|
|
10256
10369
|
data: formatDesc$1(rep)
|
|
10257
10370
|
}];
|
|
10258
10371
|
const results = findings.map((f) => {
|
|
10259
|
-
|
|
10372
|
+
const status = hasStatus ? getStatus$2(f["Result Status"] ?? "") : ResultStatus.Failed;
|
|
10373
|
+
return createResult(status, void 0, {
|
|
10260
10374
|
codeDesc: f["Details"] ?? "",
|
|
10261
10375
|
startTime: parseDate(f["Date"] ?? ""),
|
|
10262
10376
|
backtrace: getBacktrace(f["Result Status"] ?? "")
|
|
@@ -10302,10 +10416,12 @@ async function convertDbprotectToHdf(input, converterVersion = "1.0.0") {
|
|
|
10302
10416
|
const requirements = [];
|
|
10303
10417
|
for (const [checkID, findingGroup] of groups) requirements.push(buildRequirement$10(checkID, findingGroup, hasStatus));
|
|
10304
10418
|
const firstFinding = limitedFindings[0];
|
|
10419
|
+
const title = firstFinding["Job Name"] ?? "";
|
|
10420
|
+
const summary = formatSummary(firstFinding);
|
|
10305
10421
|
const baseline = createMinimalBaseline("DBProtect Scan", requirements, {
|
|
10306
10422
|
resultsChecksum,
|
|
10307
|
-
title
|
|
10308
|
-
summary
|
|
10423
|
+
title,
|
|
10424
|
+
summary
|
|
10309
10425
|
});
|
|
10310
10426
|
const policy = firstFinding["Policy"] ?? "";
|
|
10311
10427
|
if (policy) baseline.version = policy;
|
|
@@ -10323,7 +10439,9 @@ async function convertDbprotectToHdf(input, converterVersion = "1.0.0") {
|
|
|
10323
10439
|
//#region converters/twistlock-to-hdf/typescript/converter.ts
|
|
10324
10440
|
const TWISTLOCK_ALIASES = {
|
|
10325
10441
|
important: .9,
|
|
10326
|
-
moderate: .5
|
|
10442
|
+
moderate: .5,
|
|
10443
|
+
unimportant: .3,
|
|
10444
|
+
negligible: 0
|
|
10327
10445
|
};
|
|
10328
10446
|
function twistlockSeverityToImpact(severity) {
|
|
10329
10447
|
return severityToImpactWithAliases(severity, TWISTLOCK_ALIASES, .5);
|
|
@@ -10551,10 +10669,12 @@ function convertSingleResult(result, resultsChecksum) {
|
|
|
10551
10669
|
const target = result.name ?? result.repository ?? result.id ?? "scan target";
|
|
10552
10670
|
requirements.push(buildNoFindingsRequirement("twistlock-no-findings", `Twistlock scanned ${target} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
|
|
10553
10671
|
}
|
|
10672
|
+
const title = buildTitle$1(result);
|
|
10673
|
+
const summary = buildSummary(result);
|
|
10554
10674
|
return createMinimalBaseline("Twistlock Scan", requirements, {
|
|
10555
10675
|
resultsChecksum,
|
|
10556
|
-
title
|
|
10557
|
-
summary
|
|
10676
|
+
title,
|
|
10677
|
+
summary
|
|
10558
10678
|
});
|
|
10559
10679
|
}
|
|
10560
10680
|
/**
|
|
@@ -10802,9 +10922,10 @@ async function convertDeptrackToHdf(input, converterVersion = "1.0.0") {
|
|
|
10802
10922
|
const projectName = parsed.project?.name ?? parsed.project?.uuid ?? "";
|
|
10803
10923
|
requirements.push(buildNoFindingsRequirement("deptrack-no-findings", `Dependency-Track analyzed ${projectName} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
|
|
10804
10924
|
}
|
|
10925
|
+
const title = `Dependency-Track: ${parsed.project?.name ?? ""} ${parsed.project?.version ?? ""}`;
|
|
10805
10926
|
const baseline = createMinimalBaseline("Dependency-Track Scan", requirements, {
|
|
10806
10927
|
resultsChecksum,
|
|
10807
|
-
title
|
|
10928
|
+
title,
|
|
10808
10929
|
summary: parsed.project?.description
|
|
10809
10930
|
});
|
|
10810
10931
|
const targetName = parsed.project?.name ?? parsed.project?.uuid ?? "";
|
|
@@ -11383,9 +11504,10 @@ async function convertNeuvectorToHdf(input, converterVersion = "1.0.0") {
|
|
|
11383
11504
|
}
|
|
11384
11505
|
const target = targetNameFromReport(scan.report);
|
|
11385
11506
|
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("neuvector-no-findings", `NeuVector scanned ${target} and reported zero vulnerable components.`, scanTime));
|
|
11507
|
+
const title = imageTitle(scan.report);
|
|
11386
11508
|
const baseline = createMinimalBaseline("NeuVector Scan", requirements, {
|
|
11387
11509
|
resultsChecksum,
|
|
11388
|
-
title
|
|
11510
|
+
title
|
|
11389
11511
|
});
|
|
11390
11512
|
const cmds = scan.report.cmds ?? [];
|
|
11391
11513
|
if (cmds.length > 0) baseline.extensions = { neuvector: { cmds } };
|
|
@@ -11624,12 +11746,15 @@ async function convertFortifyToHdf(input, converterVersion = "1.0.0") {
|
|
|
11624
11746
|
});
|
|
11625
11747
|
const targetName = fvdl.Build?.SourceBasePath ?? fvdl.Build?.BuildID ?? "Unknown";
|
|
11626
11748
|
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("fortify-no-findings", `Fortify scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
11749
|
+
const title = "Fortify Static Analyzer Scan";
|
|
11750
|
+
const summary = `Fortify Static Analyzer Scan of UUID: ${fvdl.UUID ?? ""}`;
|
|
11751
|
+
const version = fvdl.EngineData?.EngineVersion ?? "";
|
|
11627
11752
|
const hdfResult = {
|
|
11628
11753
|
baselines: [createMinimalBaseline("Fortify Scan", requirements, {
|
|
11629
11754
|
resultsChecksum,
|
|
11630
|
-
title
|
|
11631
|
-
summary
|
|
11632
|
-
version
|
|
11755
|
+
title,
|
|
11756
|
+
summary,
|
|
11757
|
+
version,
|
|
11633
11758
|
status: "loaded"
|
|
11634
11759
|
})],
|
|
11635
11760
|
components: [{
|
|
@@ -11875,9 +12000,11 @@ function groupByHostname(records) {
|
|
|
11875
12000
|
* Builds an HDF baseline from all records for a single host.
|
|
11876
12001
|
*/
|
|
11877
12002
|
function buildBaseline(hostname, records, resultsChecksum, scanTime) {
|
|
11878
|
-
|
|
12003
|
+
const requirements = limitArrayWithWarning(records, "finding").map((rec) => buildRequirement$4(rec, scanTime));
|
|
12004
|
+
const title = `Prisma Cloud Scan (${hostname})`;
|
|
12005
|
+
return createMinimalBaseline("Prisma Cloud Scan", requirements, {
|
|
11879
12006
|
resultsChecksum,
|
|
11880
|
-
title
|
|
12007
|
+
title
|
|
11881
12008
|
});
|
|
11882
12009
|
}
|
|
11883
12010
|
/**
|
|
@@ -12143,6 +12270,7 @@ async function convertNetsparkerToHdf(input, converterVersion = "1.0.0") {
|
|
|
12143
12270
|
const startTime = (initiated ? parseNetsparkerTimestamp(initiated) : null) ?? /* @__PURE__ */ new Date();
|
|
12144
12271
|
requirements.push(buildNoFindingsRequirement("netsparker-no-findings", `${toolName} scanned ${targetName} and reported zero findings.`, startTime));
|
|
12145
12272
|
}
|
|
12273
|
+
const title = `${toolName} Enterprise Scan ID: ${target["scan-id"] ?? ""} URL: ${target.url ?? ""}`;
|
|
12146
12274
|
return buildHdfResults({
|
|
12147
12275
|
generatorName: "netsparker-to-hdf",
|
|
12148
12276
|
converterVersion,
|
|
@@ -12150,7 +12278,7 @@ async function convertNetsparkerToHdf(input, converterVersion = "1.0.0") {
|
|
|
12150
12278
|
timestamp,
|
|
12151
12279
|
baselines: [createMinimalBaseline("Netsparker Scan", requirements, {
|
|
12152
12280
|
resultsChecksum,
|
|
12153
|
-
title
|
|
12281
|
+
title
|
|
12154
12282
|
})],
|
|
12155
12283
|
components: [{
|
|
12156
12284
|
name: targetName,
|
|
@@ -12222,7 +12350,9 @@ function getNistControls(ruleID) {
|
|
|
12222
12350
|
return ctrl.split("|");
|
|
12223
12351
|
}
|
|
12224
12352
|
/**
|
|
12225
|
-
*
|
|
12353
|
+
* Flattens every service's findings into one ordered list, services then rule
|
|
12354
|
+
* keys sorted for deterministic output. Each entry stands alone, so no finding
|
|
12355
|
+
* can be displaced by another service's.
|
|
12226
12356
|
*/
|
|
12227
12357
|
function collapseFindings(report) {
|
|
12228
12358
|
const result = [];
|
|
@@ -12235,15 +12365,33 @@ function collapseFindings(report) {
|
|
|
12235
12365
|
const ruleNames = Object.keys(svc.findings).sort();
|
|
12236
12366
|
for (const ruleName of ruleNames) {
|
|
12237
12367
|
const finding = svc.findings[ruleName];
|
|
12238
|
-
if (finding) result.push(
|
|
12368
|
+
if (finding) result.push({
|
|
12369
|
+
service: serviceName,
|
|
12370
|
+
ruleID: ruleName,
|
|
12371
|
+
finding
|
|
12372
|
+
});
|
|
12239
12373
|
}
|
|
12240
12374
|
}
|
|
12241
12375
|
return result;
|
|
12242
12376
|
}
|
|
12243
12377
|
/**
|
|
12378
|
+
* Returns the HDF requirement ID for each finding. A rule key is the ID on its
|
|
12379
|
+
* own, which is what every ScoutSuite report produces: a key names one rule file,
|
|
12380
|
+
* whose path binds it to a single service. Should a key arrive under two services
|
|
12381
|
+
* anyway, both are qualified with their service so the IDs stay unique —
|
|
12382
|
+
* qualifying only on collision keeps ordinary output unchanged rather than
|
|
12383
|
+
* renaming every requirement for a case that does not occur. Mirrors Go's
|
|
12384
|
+
* assignRequirementIDs.
|
|
12385
|
+
*/
|
|
12386
|
+
function assignRequirementIDs(found) {
|
|
12387
|
+
const occurrences = /* @__PURE__ */ new Map();
|
|
12388
|
+
for (const sf of found) occurrences.set(sf.ruleID, (occurrences.get(sf.ruleID) ?? 0) + 1);
|
|
12389
|
+
return found.map((sf) => (occurrences.get(sf.ruleID) ?? 0) > 1 ? `${sf.service}:${sf.ruleID}` : sf.ruleID);
|
|
12390
|
+
}
|
|
12391
|
+
/**
|
|
12244
12392
|
* Builds a single EvaluatedRequirement from a ScoutSuite finding.
|
|
12245
12393
|
*/
|
|
12246
|
-
function buildRequirement$2(ruleID, finding, startTime) {
|
|
12394
|
+
function buildRequirement$2(id, ruleID, finding, startTime) {
|
|
12247
12395
|
const nist = getNistControls(ruleID);
|
|
12248
12396
|
const tags = buildNistCciTags(nist, nistToCci(nist));
|
|
12249
12397
|
const compliance = complianceStrings(finding.compliance);
|
|
@@ -12258,11 +12406,13 @@ function buildRequirement$2(ruleID, finding, startTime) {
|
|
|
12258
12406
|
});
|
|
12259
12407
|
const refs = [];
|
|
12260
12408
|
for (const url of finding.references ?? []) if (url) refs.push({ url });
|
|
12261
|
-
const
|
|
12409
|
+
const status = getStatus$1(finding.checked_items, finding.flagged_items);
|
|
12410
|
+
const message = getMessage(finding.checked_items, finding.flagged_items, finding.items);
|
|
12411
|
+
const resultObj = createResult(status, message, {
|
|
12262
12412
|
codeDesc: finding.description,
|
|
12263
12413
|
startTime: (startTime ? parseTimestamp(startTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
|
|
12264
12414
|
});
|
|
12265
|
-
const req = createRequirement(
|
|
12415
|
+
const req = createRequirement(id, finding.description, descriptions, getImpact$1(finding.level), [resultObj], { tags });
|
|
12266
12416
|
const controlType = deriveControlTypeFromTags(nist);
|
|
12267
12417
|
if (controlType !== void 0) req.controlType = controlType;
|
|
12268
12418
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
@@ -12284,12 +12434,15 @@ async function convertScoutsuiteToHdf(input, converterVersion = "1.0.0") {
|
|
|
12284
12434
|
const resultsChecksum = await inputChecksum(jsonStr);
|
|
12285
12435
|
const report = parseJSON(jsonStr);
|
|
12286
12436
|
if (!report || typeof report !== "object") throw new Error("scoutsuite: invalid JSON");
|
|
12287
|
-
const
|
|
12437
|
+
const limitedPairs = limitArrayWithWarning(collapseFindings(report), "finding");
|
|
12438
|
+
const ids = assignRequirementIDs(limitedPairs);
|
|
12439
|
+
const requirements = limitedPairs.map((sf, i) => buildRequirement$2(ids[i], sf.ruleID, sf.finding, report.last_run.time));
|
|
12288
12440
|
const targetName = `${report.last_run.ruleset_name} ruleset:${report.provider_name}:${report.account_id}`;
|
|
12289
12441
|
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("scoutsuite-no-findings", `ScoutSuite scanned ${targetName} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
12442
|
+
const title = `Scout Suite Report using ${report.last_run.ruleset_name} ruleset on ${report.provider_name} with account ${report.account_id}`;
|
|
12290
12443
|
const baseline = createMinimalBaseline("ScoutSuite Scan", requirements, {
|
|
12291
12444
|
resultsChecksum,
|
|
12292
|
-
title
|
|
12445
|
+
title,
|
|
12293
12446
|
summary: report.last_run.ruleset_about
|
|
12294
12447
|
});
|
|
12295
12448
|
return buildHdfResults({
|
|
@@ -12357,17 +12510,17 @@ function buildCodeDesc$1(section, scannerName) {
|
|
|
12357
12510
|
parts.push(`body:${bodyToString(section.body)}`);
|
|
12358
12511
|
parts.push(`body_format:${section.body_format ?? ""}`);
|
|
12359
12512
|
parts.push(`classification:${section.classification ?? ""}`);
|
|
12360
|
-
parts.push(`depth:${section.depth ?? 0}`);
|
|
12513
|
+
parts.push(`depth:${formatJsonNumber(section.depth ?? 0)}`);
|
|
12361
12514
|
if (section.heuristic) {
|
|
12362
12515
|
parts.push(`heuristic_heur_id:${section.heuristic.heur_id ?? ""}`);
|
|
12363
|
-
parts.push(`heuristic_score:${section.heuristic.score ?? 0}`);
|
|
12516
|
+
parts.push(`heuristic_score:${formatJsonNumber(section.heuristic.score ?? 0)}`);
|
|
12364
12517
|
parts.push(`heuristic_name:${section.heuristic.name ?? ""}`);
|
|
12365
12518
|
}
|
|
12366
12519
|
} else if (scannerName === "CodeQuality") {
|
|
12367
12520
|
parts.push(`body:${bodyToString(section.body)}`);
|
|
12368
12521
|
parts.push(`body_format:${section.body_format ?? ""}`);
|
|
12369
12522
|
parts.push(`classification:${section.classification ?? ""}`);
|
|
12370
|
-
parts.push(`depth:${section.depth ?? 0}`);
|
|
12523
|
+
parts.push(`depth:${formatJsonNumber(section.depth ?? 0)}`);
|
|
12371
12524
|
parts.push(`title_text:${section.title_text ?? ""}`);
|
|
12372
12525
|
} else return JSON.stringify(section);
|
|
12373
12526
|
return parts.join("\n");
|
|
@@ -12475,11 +12628,13 @@ function buildRequirementFromResult(result, filename) {
|
|
|
12475
12628
|
* Builds an HDF baseline for a single scanner's results.
|
|
12476
12629
|
*/
|
|
12477
12630
|
function buildScannerBaseline(scannerName, results, shaMap, resultsChecksum) {
|
|
12478
|
-
|
|
12631
|
+
const requirements = results.map((result) => {
|
|
12479
12632
|
return buildRequirementFromResult(result, shaMap.get(result.sha256) ?? "");
|
|
12480
|
-
})
|
|
12633
|
+
});
|
|
12634
|
+
const title = `Conveyor Scan (${scannerName})`;
|
|
12635
|
+
return createMinimalBaseline("Conveyor Scan", requirements, {
|
|
12481
12636
|
resultsChecksum,
|
|
12482
|
-
title
|
|
12637
|
+
title
|
|
12483
12638
|
});
|
|
12484
12639
|
}
|
|
12485
12640
|
/**
|
|
@@ -12574,7 +12729,8 @@ function decodeXmlEntities(s) {
|
|
|
12574
12729
|
/** Parse Veracode timestamp format ("2021-12-29 22:16:36 UTC") to Date. */
|
|
12575
12730
|
function parseVeracodeTimestamp(ts) {
|
|
12576
12731
|
if (!ts) return void 0;
|
|
12577
|
-
|
|
12732
|
+
const normalized = ts.replace(" UTC", "Z").replace(" ", "T");
|
|
12733
|
+
return parseTimestamp(normalized) ?? void 0;
|
|
12578
12734
|
}
|
|
12579
12735
|
/** Format description paragraphs into text. */
|
|
12580
12736
|
function formatDesc(desc) {
|
|
@@ -13095,11 +13251,12 @@ function buildRequirement$1(cs, profiles, createdDateTime) {
|
|
|
13095
13251
|
}
|
|
13096
13252
|
if (cs.on === "true") tags.on = true;
|
|
13097
13253
|
else if (cs.on === "false") tags.on = false;
|
|
13098
|
-
const
|
|
13254
|
+
const results = [{
|
|
13099
13255
|
status,
|
|
13100
13256
|
codeDesc: cs.implementationStatus || "No implementation status provided",
|
|
13101
13257
|
startTime: parseTimestamp(cs.lastSynced ?? "") ?? parseTimestamp(createdDateTime ?? "") ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
|
|
13102
|
-
}]
|
|
13258
|
+
}];
|
|
13259
|
+
const req = createRequirement(id, title, descriptions, impact, results, {
|
|
13103
13260
|
tags,
|
|
13104
13261
|
...refs.length > 0 ? { refs } : {}
|
|
13105
13262
|
});
|
|
@@ -13464,9 +13621,10 @@ function formatEvidence$1(evidence) {
|
|
|
13464
13621
|
for (const ev of evidence) {
|
|
13465
13622
|
const odataType = ev["@odata.type"] ?? "";
|
|
13466
13623
|
if (odataType.includes("deviceEvidence")) parts.push(`Device: ${ev.deviceDnsName ?? "unknown"} (OS: ${ev.osPlatform ?? "unknown"})`);
|
|
13467
|
-
else if (odataType.includes("processEvidence"))
|
|
13468
|
-
|
|
13469
|
-
|
|
13624
|
+
else if (odataType.includes("processEvidence")) {
|
|
13625
|
+
if (ev.imageFile) parts.push(`Process: ${ev.imageFile.filePath ?? ""}\\${ev.imageFile.fileName ?? ""} (Command: ${ev.processCommandLine ?? ""})`);
|
|
13626
|
+
else parts.push(`Process: (Command: ${ev.processCommandLine ?? ""})`);
|
|
13627
|
+
} else if (odataType.includes("fileEvidence")) parts.push(`File: ${ev.filePath ?? ""}\\${ev.fileName ?? ""}`);
|
|
13470
13628
|
else parts.push(`Evidence type: ${odataType}`);
|
|
13471
13629
|
}
|
|
13472
13630
|
return parts.join("\n");
|
|
@@ -13671,12 +13829,18 @@ function convertHdfToXccdf(input) {
|
|
|
13671
13829
|
const hdfData = parseHdf(input);
|
|
13672
13830
|
if (!hdfData || typeof hdfData !== "object" || !("baselines" in hdfData)) throw new Error("Invalid HDF structure: missing baselines field");
|
|
13673
13831
|
if (!Array.isArray(hdfData.baselines)) throw new Error("Invalid HDF structure: baselines must be an array");
|
|
13674
|
-
|
|
13832
|
+
const xmlObj = { Benchmark: buildBenchmarkObj(hdfData) };
|
|
13833
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n${buildXml(xmlObj, BUILD_OPTIONS)}`;
|
|
13675
13834
|
}
|
|
13676
13835
|
/** Wrap a primitive value to force it as a text node in XML output. */
|
|
13677
13836
|
function wrap(value) {
|
|
13678
13837
|
return { "#text": value };
|
|
13679
13838
|
}
|
|
13839
|
+
/** Map an XCCDF result to the severity of its message: an error result is the
|
|
13840
|
+
* checking engine reporting a fault, any other is check output. */
|
|
13841
|
+
function messageSeverity(status) {
|
|
13842
|
+
return status === "error" ? "error" : "info";
|
|
13843
|
+
}
|
|
13680
13844
|
/** Map HDF impact (0.0-1.0) to XCCDF severity via the shared band mapper. The
|
|
13681
13845
|
* XCCDF vocabulary has no critical band, so critical folds into high, and its
|
|
13682
13846
|
* zero band is spelled 'info'. */
|
|
@@ -13904,7 +14068,8 @@ function resultTimeWindow(baseline) {
|
|
|
13904
14068
|
let first;
|
|
13905
14069
|
let last;
|
|
13906
14070
|
for (const req of baseline.requirements) for (const res of req.results) {
|
|
13907
|
-
const
|
|
14071
|
+
const text = typeof res.startTime === "string" ? res.startTime : res.startTime.toISOString();
|
|
14072
|
+
const parsed = parseTimestamp(text);
|
|
13908
14073
|
if (!parsed) continue;
|
|
13909
14074
|
const at = parsed.getTime();
|
|
13910
14075
|
const canonical = formatTimestamp(parsed);
|
|
@@ -13972,7 +14137,10 @@ function buildTestResultObj(hdfData, baseline) {
|
|
|
13972
14137
|
rr[`${ATTR}time`] = parsedStart ? formatTimestamp(parsedStart) : rawStart;
|
|
13973
14138
|
if (stigId) rr[`${ATTR}version`] = stigId;
|
|
13974
14139
|
rr.result = wrap(status);
|
|
13975
|
-
if (result.message) rr.message =
|
|
14140
|
+
if (result.message) rr.message = {
|
|
14141
|
+
[`${ATTR}severity`]: messageSeverity(status),
|
|
14142
|
+
"#text": result.message
|
|
14143
|
+
};
|
|
13976
14144
|
if (result.codeDesc) rr.check = {
|
|
13977
14145
|
[`${ATTR}system`]: "http://oval.mitre.org/XMLSchema/oval-definitions-5",
|
|
13978
14146
|
"check-content": wrap(result.codeDesc)
|
|
@@ -13990,6 +14158,752 @@ function buildTestResultObj(hdfData, baseline) {
|
|
|
13990
14158
|
return testResult;
|
|
13991
14159
|
}
|
|
13992
14160
|
//#endregion
|
|
14161
|
+
//#region converters/oscal-to-hdf/go/oscal-vocabulary.json
|
|
14162
|
+
var oscal_vocabulary_default = {
|
|
14163
|
+
$comment: "The HDF OSCAL extension vocabulary (ADR-0014 §1.5): every prop an hdf-libs OSCAL exporter emits or importer reads by name, with the namespace it belongs to. The Go oscal package embeds this file and the TypeScript peer imports it; it is the only place a prop name or the HDF namespace URI is defined. 'objects' are OSCAL JSON paths; 'hdfField' is the HDF path the prop carries, or null when it carries none; 'legacy' is true exactly for the names exporters emitted without ns before ADR-0014, which the read helpers accept without ns for one minor release (§1.4).",
|
|
14164
|
+
namespace: "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14165
|
+
defaultNamespace: "http://csrc.nist.gov/ns/oscal",
|
|
14166
|
+
props: [
|
|
14167
|
+
{
|
|
14168
|
+
"name": "hdf-requirement-id",
|
|
14169
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14170
|
+
"objects": ["assessment-results.results[].findings[]", "plan-of-action-and-milestones.risks[]"],
|
|
14171
|
+
"meaning": "The HDF requirement id the finding assesses, or the requirement id of the override the POA&M risk represents, exactly as the source recorded it; omitted when the id is empty. The finding target-id carries an OSCAL token encoding of it, which is not injective, and the POA&M item and risk titles are display text.",
|
|
14172
|
+
"valueFormat": "String (§1.7)",
|
|
14173
|
+
"hdfField": "baselines[].requirements[].id, overrides[].requirementId",
|
|
14174
|
+
"legacy": true
|
|
14175
|
+
},
|
|
14176
|
+
{
|
|
14177
|
+
"name": "baseline-version",
|
|
14178
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14179
|
+
"objects": ["assessment-results.results[]"],
|
|
14180
|
+
"meaning": "The version of the HDF baseline the result reports.",
|
|
14181
|
+
"valueFormat": "String (§1.7)",
|
|
14182
|
+
"hdfField": "baselines[].version",
|
|
14183
|
+
"legacy": true
|
|
14184
|
+
},
|
|
14185
|
+
{
|
|
14186
|
+
"name": "nist",
|
|
14187
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14188
|
+
"objects": ["assessment-results.results[].findings[]"],
|
|
14189
|
+
"meaning": "A NIST SP 800-53 control the requirement maps to, one prop per mapping, in source order.",
|
|
14190
|
+
"valueFormat": "String (§1.7), NIST control notation as the source spelled it",
|
|
14191
|
+
"hdfField": "baselines[].requirements[].tags.nist[]",
|
|
14192
|
+
"legacy": true
|
|
14193
|
+
},
|
|
14194
|
+
{
|
|
14195
|
+
"name": "cci",
|
|
14196
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14197
|
+
"objects": ["assessment-results.results[].findings[]"],
|
|
14198
|
+
"meaning": "A DISA Control Correlation Identifier the requirement maps to, one prop per mapping, in source order.",
|
|
14199
|
+
"valueFormat": "String (§1.7)",
|
|
14200
|
+
"hdfField": "baselines[].requirements[].tags.cci[]",
|
|
14201
|
+
"legacy": true
|
|
14202
|
+
},
|
|
14203
|
+
{
|
|
14204
|
+
"name": "control-type",
|
|
14205
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14206
|
+
"objects": ["assessment-results.results[].findings[]"],
|
|
14207
|
+
"meaning": "The requirement's control type.",
|
|
14208
|
+
"valueFormat": "One of policy, procedure, technical, management, operational",
|
|
14209
|
+
"hdfField": "baselines[].requirements[].controlType",
|
|
14210
|
+
"legacy": true
|
|
14211
|
+
},
|
|
14212
|
+
{
|
|
14213
|
+
"name": "verification-method",
|
|
14214
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14215
|
+
"objects": ["assessment-results.results[].findings[]"],
|
|
14216
|
+
"meaning": "How the requirement is verified.",
|
|
14217
|
+
"valueFormat": "One of automated, manual-by-design, manual-pending-automation, hybrid",
|
|
14218
|
+
"hdfField": "baselines[].requirements[].verificationMethod",
|
|
14219
|
+
"legacy": true
|
|
14220
|
+
},
|
|
14221
|
+
{
|
|
14222
|
+
"name": "applicability",
|
|
14223
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14224
|
+
"objects": ["assessment-results.results[].findings[]"],
|
|
14225
|
+
"meaning": "Whether the requirement is required, optional or advisory.",
|
|
14226
|
+
"valueFormat": "One of required, optional, advisory",
|
|
14227
|
+
"hdfField": "baselines[].requirements[].applicability",
|
|
14228
|
+
"legacy": true
|
|
14229
|
+
},
|
|
14230
|
+
{
|
|
14231
|
+
"name": "cwe",
|
|
14232
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14233
|
+
"objects": ["assessment-results.results[].findings[]"],
|
|
14234
|
+
"meaning": "A CWE identifier associated with the requirement, one prop per identifier, in source order.",
|
|
14235
|
+
"valueFormat": "String (§1.7)",
|
|
14236
|
+
"hdfField": "baselines[].requirements[].cwe[]",
|
|
14237
|
+
"legacy": true
|
|
14238
|
+
},
|
|
14239
|
+
{
|
|
14240
|
+
"name": "epss-score",
|
|
14241
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14242
|
+
"objects": ["assessment-results.results[].findings[]"],
|
|
14243
|
+
"meaning": "The EPSS exploitation probability score.",
|
|
14244
|
+
"valueFormat": "Decimal number from 0 to 1",
|
|
14245
|
+
"hdfField": "baselines[].requirements[].epss.score",
|
|
14246
|
+
"legacy": true
|
|
14247
|
+
},
|
|
14248
|
+
{
|
|
14249
|
+
"name": "epss-percentile",
|
|
14250
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14251
|
+
"objects": ["assessment-results.results[].findings[]"],
|
|
14252
|
+
"meaning": "The EPSS percentile of the score.",
|
|
14253
|
+
"valueFormat": "Decimal number from 0 to 1",
|
|
14254
|
+
"hdfField": "baselines[].requirements[].epss.percentile",
|
|
14255
|
+
"legacy": true
|
|
14256
|
+
},
|
|
14257
|
+
{
|
|
14258
|
+
"name": "kev",
|
|
14259
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14260
|
+
"objects": ["assessment-results.results[].findings[]"],
|
|
14261
|
+
"meaning": "The vulnerability is in the CISA Known Exploited Vulnerabilities catalog; emitted only when it is.",
|
|
14262
|
+
"valueFormat": "The literal true",
|
|
14263
|
+
"hdfField": "baselines[].requirements[].kev.inKev",
|
|
14264
|
+
"legacy": true
|
|
14265
|
+
},
|
|
14266
|
+
{
|
|
14267
|
+
"name": "kev-due-date",
|
|
14268
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14269
|
+
"objects": ["assessment-results.results[].findings[]"],
|
|
14270
|
+
"meaning": "The remediation due date the KEV catalog sets for the vulnerability.",
|
|
14271
|
+
"valueFormat": "String (§1.7), a date as the source recorded it",
|
|
14272
|
+
"hdfField": "baselines[].requirements[].kev.dueDate",
|
|
14273
|
+
"legacy": true
|
|
14274
|
+
},
|
|
14275
|
+
{
|
|
14276
|
+
"name": "cvss-base-score",
|
|
14277
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14278
|
+
"objects": ["assessment-results.results[].findings[]"],
|
|
14279
|
+
"meaning": "A CVSS base score, one prop per CVSS record that carries one, in source order.",
|
|
14280
|
+
"valueFormat": "Decimal number from 0 to 10",
|
|
14281
|
+
"hdfField": "baselines[].requirements[].cvss[].baseScore",
|
|
14282
|
+
"legacy": true
|
|
14283
|
+
},
|
|
14284
|
+
{
|
|
14285
|
+
"name": "cvss-base-vector",
|
|
14286
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14287
|
+
"objects": ["assessment-results.results[].findings[]"],
|
|
14288
|
+
"meaning": "A CVSS base vector, one prop per CVSS record that carries one, in source order.",
|
|
14289
|
+
"valueFormat": "String (§1.7), a CVSS vector string",
|
|
14290
|
+
"hdfField": "baselines[].requirements[].cvss[].baseVector",
|
|
14291
|
+
"legacy": true
|
|
14292
|
+
},
|
|
14293
|
+
{
|
|
14294
|
+
"name": "reference",
|
|
14295
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14296
|
+
"objects": ["assessment-results.results[].findings[]"],
|
|
14297
|
+
"meaning": "A requirement reference that is plain text rather than a URL or URI (those become finding links).",
|
|
14298
|
+
"valueFormat": "String (§1.7)",
|
|
14299
|
+
"hdfField": "baselines[].requirements[].refs[].ref",
|
|
14300
|
+
"legacy": true
|
|
14301
|
+
},
|
|
14302
|
+
{
|
|
14303
|
+
"name": "check",
|
|
14304
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14305
|
+
"objects": ["assessment-results.results[].findings[]"],
|
|
14306
|
+
"meaning": "Pre-ADR only: a one-line preview of the requirement's check text, with the full text in remarks. No longer emitted; the check text is carried in observation relevant-evidence marked with description-label.",
|
|
14307
|
+
"valueFormat": "String (§1.7)",
|
|
14308
|
+
"hdfField": "baselines[].requirements[].descriptions[label=check].data",
|
|
14309
|
+
"legacy": true
|
|
14310
|
+
},
|
|
14311
|
+
{
|
|
14312
|
+
"name": "fix",
|
|
14313
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14314
|
+
"objects": ["assessment-results.results[].findings[]"],
|
|
14315
|
+
"meaning": "Pre-ADR only: a one-line preview of the requirement's fix text when no risk was emitted, with the full text in remarks. No longer emitted; the fix text is carried in a risk remediation or observation relevant-evidence marked with description-label.",
|
|
14316
|
+
"valueFormat": "String (§1.7)",
|
|
14317
|
+
"hdfField": "baselines[].requirements[].descriptions[label=fix].data",
|
|
14318
|
+
"legacy": true
|
|
14319
|
+
},
|
|
14320
|
+
{
|
|
14321
|
+
"name": "rationale",
|
|
14322
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14323
|
+
"objects": ["assessment-results.results[].findings[]"],
|
|
14324
|
+
"meaning": "Pre-ADR only: a one-line preview of the requirement's rationale text, with the full text in remarks. No longer emitted; the rationale is carried in the finding target description.",
|
|
14325
|
+
"valueFormat": "String (§1.7)",
|
|
14326
|
+
"hdfField": "baselines[].requirements[].descriptions[label=rationale].data",
|
|
14327
|
+
"legacy": true
|
|
14328
|
+
},
|
|
14329
|
+
{
|
|
14330
|
+
"name": "description-label",
|
|
14331
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14332
|
+
"objects": ["assessment-results.results[].observations[].relevant-evidence[]", "assessment-results.results[].risks[].remediations[]"],
|
|
14333
|
+
"meaning": "Marks the object as carrying the text of the HDF description with this label; evidence entries hold the full text in remarks, remediations in description.",
|
|
14334
|
+
"valueFormat": "One of check, fix",
|
|
14335
|
+
"hdfField": "baselines[].requirements[].descriptions[].label",
|
|
14336
|
+
"legacy": false
|
|
14337
|
+
},
|
|
14338
|
+
{
|
|
14339
|
+
"name": "empty-field",
|
|
14340
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14341
|
+
"objects": ["Any object that carries HDF props"],
|
|
14342
|
+
"meaning": "An optional HDF string field that is present but empty; the importer returns it present and empty (§1.7.3).",
|
|
14343
|
+
"valueFormat": "The HDF JSON property name, dot-separated relative to the OSCAL object that carries the marker (e.g. systemRef, checksum.value); for props that share a group, the field within that group (e.g. key, fixedInVersion) (§1.7.5)",
|
|
14344
|
+
"hdfField": null,
|
|
14345
|
+
"legacy": false
|
|
14346
|
+
},
|
|
14347
|
+
{
|
|
14348
|
+
"name": "absent-field",
|
|
14349
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14350
|
+
"objects": ["Any object that carries HDF props"],
|
|
14351
|
+
"meaning": "OSCAL requires a value the HDF field does not have, so the object holds display fallback text; the importer leaves the field absent and never reads the fallback as HDF data (§1.7.4).",
|
|
14352
|
+
"valueFormat": "The HDF JSON property name, dot-separated relative to the OSCAL object that carries the marker (e.g. systemRef, checksum.value); for props that share a group, the field within that group (e.g. key, fixedInVersion) (§1.7.5)",
|
|
14353
|
+
"hdfField": null,
|
|
14354
|
+
"legacy": false
|
|
14355
|
+
},
|
|
14356
|
+
{
|
|
14357
|
+
"name": "amendment-id",
|
|
14358
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14359
|
+
"objects": ["plan-of-action-and-milestones.metadata"],
|
|
14360
|
+
"meaning": "The identifier of the HDF amendments document.",
|
|
14361
|
+
"valueFormat": "String (§1.7), a UUID",
|
|
14362
|
+
"hdfField": "amendmentId",
|
|
14363
|
+
"legacy": true
|
|
14364
|
+
},
|
|
14365
|
+
{
|
|
14366
|
+
"name": "override-type",
|
|
14367
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14368
|
+
"objects": ["plan-of-action-and-milestones.risks[]"],
|
|
14369
|
+
"meaning": "The type of the HDF override the risk represents. A risk carrying it in the HDF namespace is HDF-produced (ADR-0014 §4.3), so the importer reads every §4.6 field from its HDF props; a risk carrying it without ns is a pre-ADR export, which imports through the pre-ADR mapping.",
|
|
14370
|
+
"valueFormat": "An HDF override type, such as waiver, poam or falsePositive",
|
|
14371
|
+
"hdfField": "overrides[].type",
|
|
14372
|
+
"legacy": true
|
|
14373
|
+
},
|
|
14374
|
+
{
|
|
14375
|
+
"name": "impact-override",
|
|
14376
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14377
|
+
"objects": ["plan-of-action-and-milestones.risks[]"],
|
|
14378
|
+
"meaning": "The impact the override assigns to the requirement.",
|
|
14379
|
+
"valueFormat": "Decimal number from 0 to 1",
|
|
14380
|
+
"hdfField": "overrides[].impact.value",
|
|
14381
|
+
"legacy": true
|
|
14382
|
+
},
|
|
14383
|
+
{
|
|
14384
|
+
"name": "justification",
|
|
14385
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14386
|
+
"objects": ["plan-of-action-and-milestones.risks[]"],
|
|
14387
|
+
"meaning": "The controlled-vocabulary justification for the override.",
|
|
14388
|
+
"valueFormat": "An HDF override justification value",
|
|
14389
|
+
"hdfField": "overrides[].justification",
|
|
14390
|
+
"legacy": true
|
|
14391
|
+
},
|
|
14392
|
+
{
|
|
14393
|
+
"name": "baseline-ref",
|
|
14394
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14395
|
+
"objects": ["plan-of-action-and-milestones.risks[]"],
|
|
14396
|
+
"meaning": "The baseline the override is scoped to.",
|
|
14397
|
+
"valueFormat": "String (§1.7)",
|
|
14398
|
+
"hdfField": "overrides[].baselineRef",
|
|
14399
|
+
"legacy": true
|
|
14400
|
+
},
|
|
14401
|
+
{
|
|
14402
|
+
"name": "component-ref",
|
|
14403
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14404
|
+
"objects": ["plan-of-action-and-milestones.risks[]"],
|
|
14405
|
+
"meaning": "The component the override is scoped to.",
|
|
14406
|
+
"valueFormat": "String (§1.7), a UUID",
|
|
14407
|
+
"hdfField": "overrides[].componentRef",
|
|
14408
|
+
"legacy": true
|
|
14409
|
+
},
|
|
14410
|
+
{
|
|
14411
|
+
"name": "milestone-status",
|
|
14412
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14413
|
+
"objects": ["plan-of-action-and-milestones.risks[].remediations[].tasks[]"],
|
|
14414
|
+
"meaning": "The status of the milestone the remediation task represents. Documents exported before ADR-0014 carry it on the remediation, and import through the pre-ADR mapping (§4.3), which does not read it.",
|
|
14415
|
+
"valueFormat": "An HDF milestone status",
|
|
14416
|
+
"hdfField": "overrides[].milestones[].status",
|
|
14417
|
+
"legacy": true
|
|
14418
|
+
},
|
|
14419
|
+
{
|
|
14420
|
+
"name": "completed-at",
|
|
14421
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14422
|
+
"objects": ["plan-of-action-and-milestones.risks[].remediations[].tasks[]"],
|
|
14423
|
+
"meaning": "When the milestone was completed.",
|
|
14424
|
+
"valueFormat": "RFC 3339 date-time in UTC",
|
|
14425
|
+
"hdfField": "overrides[].milestones[].completedAt",
|
|
14426
|
+
"legacy": true
|
|
14427
|
+
},
|
|
14428
|
+
{
|
|
14429
|
+
"name": "completed-by",
|
|
14430
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14431
|
+
"objects": ["plan-of-action-and-milestones.risks[].remediations[].tasks[]"],
|
|
14432
|
+
"meaning": "Pre-ADR only: the identifier of whoever completed the milestone. No longer emitted; the task's responsible-roles entry with role-id completed-by names the party.",
|
|
14433
|
+
"valueFormat": "String (§1.7)",
|
|
14434
|
+
"hdfField": "overrides[].milestones[].completedBy.identifier",
|
|
14435
|
+
"legacy": true
|
|
14436
|
+
},
|
|
14437
|
+
{
|
|
14438
|
+
"name": "mime-type",
|
|
14439
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14440
|
+
"objects": ["plan-of-action-and-milestones.observations[]"],
|
|
14441
|
+
"meaning": "The media type of the evidence the observation carries.",
|
|
14442
|
+
"valueFormat": "String (§1.7), a media type",
|
|
14443
|
+
"hdfField": "overrides[].evidence[].mimeType",
|
|
14444
|
+
"legacy": true
|
|
14445
|
+
},
|
|
14446
|
+
{
|
|
14447
|
+
"name": "captured-by",
|
|
14448
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14449
|
+
"objects": ["plan-of-action-and-milestones.observations[]"],
|
|
14450
|
+
"meaning": "Pre-ADR only: the identifier of whoever captured the evidence. No longer emitted; the observation's origins actor names the party.",
|
|
14451
|
+
"valueFormat": "String (§1.7)",
|
|
14452
|
+
"hdfField": "overrides[].evidence[].capturedBy.identifier",
|
|
14453
|
+
"legacy": true
|
|
14454
|
+
},
|
|
14455
|
+
{
|
|
14456
|
+
"name": "source-name",
|
|
14457
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14458
|
+
"objects": ["plan-of-action-and-milestones.back-matter.resources[]"],
|
|
14459
|
+
"meaning": "The name of the source of the external reference the resource carries.",
|
|
14460
|
+
"valueFormat": "String (§1.7)",
|
|
14461
|
+
"hdfField": "overrides[].externalReferences[].sourceName",
|
|
14462
|
+
"legacy": true
|
|
14463
|
+
},
|
|
14464
|
+
{
|
|
14465
|
+
"name": "external-id",
|
|
14466
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14467
|
+
"objects": ["plan-of-action-and-milestones.back-matter.resources[]"],
|
|
14468
|
+
"meaning": "The identifier the source assigns to the external reference the resource carries.",
|
|
14469
|
+
"valueFormat": "String (§1.7)",
|
|
14470
|
+
"hdfField": "overrides[].externalReferences[].externalId",
|
|
14471
|
+
"legacy": true
|
|
14472
|
+
},
|
|
14473
|
+
{
|
|
14474
|
+
"name": "override-status",
|
|
14475
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14476
|
+
"objects": ["plan-of-action-and-milestones.risks[]"],
|
|
14477
|
+
"meaning": "The status the override sets, emitted only when the override sets one; the risk status is closed for passed and notApplicable and open otherwise.",
|
|
14478
|
+
"valueFormat": "An HDF result status: passed, failed, notApplicable, notReviewed or error",
|
|
14479
|
+
"hdfField": "overrides[].status",
|
|
14480
|
+
"legacy": false
|
|
14481
|
+
},
|
|
14482
|
+
{
|
|
14483
|
+
"name": "inherited-from",
|
|
14484
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14485
|
+
"objects": ["plan-of-action-and-milestones.risks[]"],
|
|
14486
|
+
"meaning": "The componentId of the local component that provides the control the override inherits.",
|
|
14487
|
+
"valueFormat": "String (§1.7), a UUID",
|
|
14488
|
+
"hdfField": "overrides[].inheritedFrom",
|
|
14489
|
+
"legacy": false
|
|
14490
|
+
},
|
|
14491
|
+
{
|
|
14492
|
+
"name": "affected-package-name",
|
|
14493
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14494
|
+
"objects": ["plan-of-action-and-milestones.risks[]"],
|
|
14495
|
+
"meaning": "The name of a package the override is scoped to; every prop of one package shares the group package-<n>, n its 1-based position in source order.",
|
|
14496
|
+
"valueFormat": "String (§1.7)",
|
|
14497
|
+
"hdfField": "overrides[].affectedPackages[].name",
|
|
14498
|
+
"legacy": false
|
|
14499
|
+
},
|
|
14500
|
+
{
|
|
14501
|
+
"name": "affected-package-version",
|
|
14502
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14503
|
+
"objects": ["plan-of-action-and-milestones.risks[]"],
|
|
14504
|
+
"meaning": "The version of a package the override is scoped to, grouped as affected-package-name.",
|
|
14505
|
+
"valueFormat": "String (§1.7)",
|
|
14506
|
+
"hdfField": "overrides[].affectedPackages[].version",
|
|
14507
|
+
"legacy": false
|
|
14508
|
+
},
|
|
14509
|
+
{
|
|
14510
|
+
"name": "affected-package-ecosystem",
|
|
14511
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14512
|
+
"objects": ["plan-of-action-and-milestones.risks[]"],
|
|
14513
|
+
"meaning": "The ecosystem of a package the override is scoped to, grouped as affected-package-name.",
|
|
14514
|
+
"valueFormat": "An HDF package ecosystem, such as npm, rpm or generic",
|
|
14515
|
+
"hdfField": "overrides[].affectedPackages[].ecosystem",
|
|
14516
|
+
"legacy": false
|
|
14517
|
+
},
|
|
14518
|
+
{
|
|
14519
|
+
"name": "affected-package-cpe",
|
|
14520
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14521
|
+
"objects": ["plan-of-action-and-milestones.risks[]"],
|
|
14522
|
+
"meaning": "The CPE 2.3 name of a package the override is scoped to, grouped as affected-package-name.",
|
|
14523
|
+
"valueFormat": "String (§1.7), a CPE 2.3 name",
|
|
14524
|
+
"hdfField": "overrides[].affectedPackages[].cpe",
|
|
14525
|
+
"legacy": false
|
|
14526
|
+
},
|
|
14527
|
+
{
|
|
14528
|
+
"name": "affected-package-purl",
|
|
14529
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14530
|
+
"objects": ["plan-of-action-and-milestones.risks[]"],
|
|
14531
|
+
"meaning": "The package URL of a package the override is scoped to, grouped as affected-package-name.",
|
|
14532
|
+
"valueFormat": "String (§1.7), a package URL",
|
|
14533
|
+
"hdfField": "overrides[].affectedPackages[].purl",
|
|
14534
|
+
"legacy": false
|
|
14535
|
+
},
|
|
14536
|
+
{
|
|
14537
|
+
"name": "affected-package-fixed-in-version",
|
|
14538
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14539
|
+
"objects": ["plan-of-action-and-milestones.risks[]"],
|
|
14540
|
+
"meaning": "The first version of a package the override is scoped to that contains the fix, grouped as affected-package-name.",
|
|
14541
|
+
"valueFormat": "String (§1.7)",
|
|
14542
|
+
"hdfField": "overrides[].affectedPackages[].fixedInVersion",
|
|
14543
|
+
"legacy": false
|
|
14544
|
+
},
|
|
14545
|
+
{
|
|
14546
|
+
"name": "evidence-encoding",
|
|
14547
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14548
|
+
"objects": ["plan-of-action-and-milestones.observations[]"],
|
|
14549
|
+
"meaning": "The encoding the evidence data declares; data whose encoding is exactly base64 is carried in the linked resource as-is.",
|
|
14550
|
+
"valueFormat": "String (§1.7)",
|
|
14551
|
+
"hdfField": "overrides[].evidence[].encoding",
|
|
14552
|
+
"legacy": false
|
|
14553
|
+
},
|
|
14554
|
+
{
|
|
14555
|
+
"name": "evidence-size",
|
|
14556
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14557
|
+
"objects": ["plan-of-action-and-milestones.observations[]"],
|
|
14558
|
+
"meaning": "The size of the evidence data in bytes.",
|
|
14559
|
+
"valueFormat": "Decimal number",
|
|
14560
|
+
"hdfField": "overrides[].evidence[].size",
|
|
14561
|
+
"legacy": false
|
|
14562
|
+
},
|
|
14563
|
+
{
|
|
14564
|
+
"name": "reference-rel",
|
|
14565
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14566
|
+
"objects": ["plan-of-action-and-milestones.back-matter.resources[]"],
|
|
14567
|
+
"meaning": "The relationship of the external reference the resource carries to the override.",
|
|
14568
|
+
"valueFormat": "String (§1.7)",
|
|
14569
|
+
"hdfField": "overrides[].externalReferences[].rel",
|
|
14570
|
+
"legacy": false
|
|
14571
|
+
},
|
|
14572
|
+
{
|
|
14573
|
+
"name": "reference-media-type",
|
|
14574
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14575
|
+
"objects": ["plan-of-action-and-milestones.back-matter.resources[]"],
|
|
14576
|
+
"meaning": "The media type of the artifact the external reference points at.",
|
|
14577
|
+
"valueFormat": "String (§1.7), a media type",
|
|
14578
|
+
"hdfField": "overrides[].externalReferences[].mediaType",
|
|
14579
|
+
"legacy": false
|
|
14580
|
+
},
|
|
14581
|
+
{
|
|
14582
|
+
"name": "checksum-algorithm",
|
|
14583
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14584
|
+
"objects": ["plan-of-action-and-milestones.back-matter.resources[]"],
|
|
14585
|
+
"meaning": "The hash algorithm of the checksum of the artifact the external reference points at.",
|
|
14586
|
+
"valueFormat": "One of sha256, sha384, sha512, blake3",
|
|
14587
|
+
"hdfField": "overrides[].externalReferences[].checksum.algorithm",
|
|
14588
|
+
"legacy": false
|
|
14589
|
+
},
|
|
14590
|
+
{
|
|
14591
|
+
"name": "checksum-value",
|
|
14592
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14593
|
+
"objects": ["plan-of-action-and-milestones.back-matter.resources[]"],
|
|
14594
|
+
"meaning": "The checksum of the artifact the external reference points at.",
|
|
14595
|
+
"valueFormat": "String (§1.7), a hex digest",
|
|
14596
|
+
"hdfField": "overrides[].externalReferences[].checksum.value",
|
|
14597
|
+
"legacy": false
|
|
14598
|
+
},
|
|
14599
|
+
{
|
|
14600
|
+
"name": "added-by",
|
|
14601
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14602
|
+
"objects": ["plan-of-action-and-milestones.back-matter.resources[]"],
|
|
14603
|
+
"meaning": "Who attached the external reference: the uuid of the metadata party carrying the identity.",
|
|
14604
|
+
"valueFormat": "A party UUID",
|
|
14605
|
+
"hdfField": "overrides[].externalReferences[].addedBy",
|
|
14606
|
+
"legacy": false
|
|
14607
|
+
},
|
|
14608
|
+
{
|
|
14609
|
+
"name": "added-at",
|
|
14610
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14611
|
+
"objects": ["plan-of-action-and-milestones.back-matter.resources[]"],
|
|
14612
|
+
"meaning": "When the external reference was attached.",
|
|
14613
|
+
"valueFormat": "RFC 3339 date-time in UTC",
|
|
14614
|
+
"hdfField": "overrides[].externalReferences[].addedAt",
|
|
14615
|
+
"legacy": false
|
|
14616
|
+
},
|
|
14617
|
+
{
|
|
14618
|
+
"name": "reference-kind",
|
|
14619
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14620
|
+
"objects": ["plan-of-action-and-milestones.back-matter.resources[]"],
|
|
14621
|
+
"meaning": "The kind of payload the external reference classifies, such as threat-intel or advisory.",
|
|
14622
|
+
"valueFormat": "String (§1.7)",
|
|
14623
|
+
"hdfField": "overrides[].externalReferences[].kind",
|
|
14624
|
+
"legacy": false
|
|
14625
|
+
},
|
|
14626
|
+
{
|
|
14627
|
+
"name": "identity-identifier",
|
|
14628
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14629
|
+
"objects": ["plan-of-action-and-milestones.metadata.parties[]"],
|
|
14630
|
+
"meaning": "The identifier of the HDF identity the party carries; omitted when the identifier is empty. The party name is display text.",
|
|
14631
|
+
"valueFormat": "String (§1.7)",
|
|
14632
|
+
"hdfField": "Identity.identifier",
|
|
14633
|
+
"legacy": false
|
|
14634
|
+
},
|
|
14635
|
+
{
|
|
14636
|
+
"name": "identity-type",
|
|
14637
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14638
|
+
"objects": ["plan-of-action-and-milestones.metadata.parties[]"],
|
|
14639
|
+
"meaning": "The type of the HDF identity the party carries; the party type is display text. The party remarks hold the identity description.",
|
|
14640
|
+
"valueFormat": "One of email, username, system, agent, simple, other",
|
|
14641
|
+
"hdfField": "Identity.type",
|
|
14642
|
+
"legacy": false
|
|
14643
|
+
},
|
|
14644
|
+
{
|
|
14645
|
+
"name": "amendments-name",
|
|
14646
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14647
|
+
"objects": ["plan-of-action-and-milestones.metadata"],
|
|
14648
|
+
"meaning": "The name of the HDF amendments document; omitted when the name is empty. The metadata title is display text.",
|
|
14649
|
+
"valueFormat": "String (§1.7)",
|
|
14650
|
+
"hdfField": "name",
|
|
14651
|
+
"legacy": false
|
|
14652
|
+
},
|
|
14653
|
+
{
|
|
14654
|
+
"name": "label-key",
|
|
14655
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14656
|
+
"objects": ["plan-of-action-and-milestones.metadata"],
|
|
14657
|
+
"meaning": "The key of an amendments label, with class amendment-label and group label-<n>, n the label's 1-based position in sorted key order.",
|
|
14658
|
+
"valueFormat": "String (§1.7)",
|
|
14659
|
+
"hdfField": "labels (key)",
|
|
14660
|
+
"legacy": false
|
|
14661
|
+
},
|
|
14662
|
+
{
|
|
14663
|
+
"name": "label-value",
|
|
14664
|
+
"ns": "https://mitre.github.io/hdf-libs/ns/oscal",
|
|
14665
|
+
"objects": ["plan-of-action-and-milestones.metadata"],
|
|
14666
|
+
"meaning": "The value of the amendments label its group names, with class amendment-label.",
|
|
14667
|
+
"valueFormat": "String (§1.7)",
|
|
14668
|
+
"hdfField": "labels (value)",
|
|
14669
|
+
"legacy": false
|
|
14670
|
+
},
|
|
14671
|
+
{
|
|
14672
|
+
"name": "impacted-control-id",
|
|
14673
|
+
"ns": "https://fedramp.gov/ns/oscal",
|
|
14674
|
+
"objects": ["plan-of-action-and-milestones.risks[]"],
|
|
14675
|
+
"meaning": "FedRAMP vocabulary: a control impacted by the POA&M item. The POA&M exporter emits it and the importer reads it as the override requirement id.",
|
|
14676
|
+
"valueFormat": "OSCAL control id token",
|
|
14677
|
+
"hdfField": "overrides[].requirementId",
|
|
14678
|
+
"legacy": true
|
|
14679
|
+
},
|
|
14680
|
+
{
|
|
14681
|
+
"name": "POAM-ID",
|
|
14682
|
+
"ns": "https://fedramp.gov/ns/oscal",
|
|
14683
|
+
"objects": ["plan-of-action-and-milestones.poam-items[]"],
|
|
14684
|
+
"meaning": "FedRAMP vocabulary: the CSP-assigned POA&M tracking identifier. The importer reads it as the override requirement id when no related risk carries impacted-control-id.",
|
|
14685
|
+
"valueFormat": "String, a CSP tracking number",
|
|
14686
|
+
"hdfField": "overrides[].requirementId",
|
|
14687
|
+
"legacy": false
|
|
14688
|
+
},
|
|
14689
|
+
{
|
|
14690
|
+
"name": "CORE",
|
|
14691
|
+
"ns": "https://fedramp.gov/ns/oscal",
|
|
14692
|
+
"objects": [
|
|
14693
|
+
"catalog.controls[]",
|
|
14694
|
+
"catalog.groups[].controls[]",
|
|
14695
|
+
"profile.modify.alters[].adds[]"
|
|
14696
|
+
],
|
|
14697
|
+
"meaning": "FedRAMP vocabulary: the control must be included in every FedRAMP assessment. The importer maps the value true to applicability required.",
|
|
14698
|
+
"valueFormat": "The literal true",
|
|
14699
|
+
"hdfField": "requirements[].applicability",
|
|
14700
|
+
"legacy": false
|
|
14701
|
+
},
|
|
14702
|
+
{
|
|
14703
|
+
"name": "assessment-type",
|
|
14704
|
+
"ns": "https://fedramp.gov/ns/oscal",
|
|
14705
|
+
"objects": ["assessment-plan.metadata"],
|
|
14706
|
+
"meaning": "FedRAMP vocabulary: the type of assessment. The importer maps automated and manual to the plan type.",
|
|
14707
|
+
"valueFormat": "String",
|
|
14708
|
+
"hdfField": "type",
|
|
14709
|
+
"legacy": false
|
|
14710
|
+
},
|
|
14711
|
+
{
|
|
14712
|
+
"name": "label",
|
|
14713
|
+
"ns": "http://csrc.nist.gov/ns/oscal",
|
|
14714
|
+
"objects": ["catalog.controls[]", "catalog.groups[].controls[]"],
|
|
14715
|
+
"meaning": "NIST vocabulary: a human-readable label for the control, which may be rendered in place of its identifier.",
|
|
14716
|
+
"valueFormat": "String",
|
|
14717
|
+
"hdfField": "requirements[].tags.label",
|
|
14718
|
+
"legacy": false
|
|
14719
|
+
},
|
|
14720
|
+
{
|
|
14721
|
+
"name": "sort-id",
|
|
14722
|
+
"ns": "http://csrc.nist.gov/ns/oscal",
|
|
14723
|
+
"objects": ["catalog.controls[]", "catalog.groups[].controls[]"],
|
|
14724
|
+
"meaning": "NIST vocabulary: an alternative identifier that sorts easily among the other controls in the document.",
|
|
14725
|
+
"valueFormat": "String",
|
|
14726
|
+
"hdfField": "requirements[].tags.sort-id",
|
|
14727
|
+
"legacy": false
|
|
14728
|
+
},
|
|
14729
|
+
{
|
|
14730
|
+
"name": "version",
|
|
14731
|
+
"ns": "http://csrc.nist.gov/ns/oscal",
|
|
14732
|
+
"objects": ["assessment-plan.assessment-assets.components[]"],
|
|
14733
|
+
"meaning": "NIST vocabulary: the version of the component. The importer reads it from the first assessment-asset component as the runner version.",
|
|
14734
|
+
"valueFormat": "String",
|
|
14735
|
+
"hdfField": "assessments[].runner.version",
|
|
14736
|
+
"legacy": false
|
|
14737
|
+
},
|
|
14738
|
+
{
|
|
14739
|
+
"name": "type",
|
|
14740
|
+
"ns": "http://csrc.nist.gov/ns/oscal",
|
|
14741
|
+
"objects": ["assessment-results.back-matter.resources[]"],
|
|
14742
|
+
"meaning": "NIST vocabulary: the kind of resource. HDF emits the value evidence on the resource holding a requirement's source code.",
|
|
14743
|
+
"valueFormat": "A NIST resource type; HDF emits evidence",
|
|
14744
|
+
"hdfField": null,
|
|
14745
|
+
"legacy": false
|
|
14746
|
+
}
|
|
14747
|
+
]
|
|
14748
|
+
};
|
|
14749
|
+
//#endregion
|
|
14750
|
+
//#region converters/oscal-to-hdf/typescript/vocabulary.ts
|
|
14751
|
+
/**
|
|
14752
|
+
* The HDF OSCAL extension vocabulary (ADR-0014 §1.5) and the helpers that emit
|
|
14753
|
+
* and read its props. The table is the JSON file the Go peer embeds, imported
|
|
14754
|
+
* directly so both languages read one file; the bundler inlines it into dist.
|
|
14755
|
+
*/
|
|
14756
|
+
/**
|
|
14757
|
+
* Validates a decoded vocabulary table and indexes its rows by name, throwing on
|
|
14758
|
+
* a malformed table. Mirrors newVocabulary in Go.
|
|
14759
|
+
*/
|
|
14760
|
+
function validateVocabularyTable(raw) {
|
|
14761
|
+
const table = raw ?? {};
|
|
14762
|
+
if (typeof table.namespace !== "string" || table.namespace === "") throw new Error("oscal: the OSCAL vocabulary table has no namespace");
|
|
14763
|
+
if (typeof table.defaultNamespace !== "string" || table.defaultNamespace === "") throw new Error("oscal: the OSCAL vocabulary table has no defaultNamespace");
|
|
14764
|
+
if (!Array.isArray(table.props) || table.props.length === 0) throw new Error("oscal: the OSCAL vocabulary table has no rows");
|
|
14765
|
+
const byName = /* @__PURE__ */ new Map();
|
|
14766
|
+
for (const row of table.props) {
|
|
14767
|
+
if (byName.has(row.name)) throw new Error(`oscal: the OSCAL vocabulary table defines "${row.name}" twice`);
|
|
14768
|
+
byName.set(row.name, row);
|
|
14769
|
+
}
|
|
14770
|
+
return {
|
|
14771
|
+
namespace: table.namespace,
|
|
14772
|
+
defaultNamespace: table.defaultNamespace,
|
|
14773
|
+
props: table.props,
|
|
14774
|
+
byName
|
|
14775
|
+
};
|
|
14776
|
+
}
|
|
14777
|
+
const table = validateVocabularyTable(oscal_vocabulary_default);
|
|
14778
|
+
const byName = table.byName;
|
|
14779
|
+
function mustVocabularyRow(name) {
|
|
14780
|
+
const row = byName.get(name);
|
|
14781
|
+
if (!row) throw new Error(`oscal: "${name}" is not a row of the OSCAL vocabulary`);
|
|
14782
|
+
return row;
|
|
14783
|
+
}
|
|
14784
|
+
/** Whether a UTF-16 code unit is ECMAScript \s whitespace, which is BMP-only. */
|
|
14785
|
+
function isWhitespace(unit) {
|
|
14786
|
+
return /\s/.test(unit);
|
|
14787
|
+
}
|
|
14788
|
+
/**
|
|
14789
|
+
* Renders an HDF value as an OSCAL StringDatatype (ADR-0014 §1.7.1): each run of
|
|
14790
|
+
* line terminators becomes one space, ECMAScript whitespace is trimmed from both
|
|
14791
|
+
* edges, and a value with nothing left becomes "_". An empty value stays empty;
|
|
14792
|
+
* carrying it is the caller's decision (§1.7.3). Mirrors NormalizePropValue in Go.
|
|
14793
|
+
*/
|
|
14794
|
+
function normalizePropValue(value) {
|
|
14795
|
+
if (value === "") return "";
|
|
14796
|
+
const joined = value.replace(/[\r\n\u2028\u2029]+/g, " ");
|
|
14797
|
+
let start = 0;
|
|
14798
|
+
while (start < joined.length && isWhitespace(joined[start])) start++;
|
|
14799
|
+
let end = joined.length;
|
|
14800
|
+
while (end > start && isWhitespace(joined[end - 1])) end--;
|
|
14801
|
+
const normalized = joined.slice(start, end);
|
|
14802
|
+
return normalized === "" ? "_" : normalized;
|
|
14803
|
+
}
|
|
14804
|
+
/**
|
|
14805
|
+
* Builds the prop a vocabulary row names, in that row's namespace (omitted for
|
|
14806
|
+
* NIST's default namespace). The value is normalized (§1.7.1), and an HDF prop
|
|
14807
|
+
* whose value changed carries the exact value in remarks (§1.7.2). Returns
|
|
14808
|
+
* undefined for an empty value, which writes no prop. An unknown name throws:
|
|
14809
|
+
* every prop an exporter emits must be a row. Mirrors VocabularyProp in Go.
|
|
14810
|
+
*/
|
|
14811
|
+
function vocabularyProp(name, value) {
|
|
14812
|
+
const row = mustVocabularyRow(name);
|
|
14813
|
+
if (value === "") return void 0;
|
|
14814
|
+
const prop = {
|
|
14815
|
+
name: row.name,
|
|
14816
|
+
value: normalizePropValue(value)
|
|
14817
|
+
};
|
|
14818
|
+
if (row.ns !== table.defaultNamespace) prop.ns = row.ns;
|
|
14819
|
+
if (row.ns === table.namespace && prop.value !== value) prop.remarks = value;
|
|
14820
|
+
return prop;
|
|
14821
|
+
}
|
|
14822
|
+
/** Pushes vocabularyProp(name, value) onto props when it writes a prop. */
|
|
14823
|
+
function pushVocabularyProp(props, name, value) {
|
|
14824
|
+
const prop = vocabularyProp(name, value);
|
|
14825
|
+
if (prop) props.push(prop);
|
|
14826
|
+
}
|
|
14827
|
+
/** Marks an optional HDF string field that is present but empty (§1.7.3). */
|
|
14828
|
+
function emptyFieldProp(field) {
|
|
14829
|
+
return mustFieldProp("empty-field", field);
|
|
14830
|
+
}
|
|
14831
|
+
/** Marks an HDF field OSCAL required a display fallback for (§1.7.4). */
|
|
14832
|
+
function absentFieldProp(field) {
|
|
14833
|
+
return mustFieldProp("absent-field", field);
|
|
14834
|
+
}
|
|
14835
|
+
function mustFieldProp(name, field) {
|
|
14836
|
+
const prop = vocabularyProp(name, field);
|
|
14837
|
+
if (!prop) throw new Error(`oscal: ${name} needs the name of an HDF field`);
|
|
14838
|
+
return prop;
|
|
14839
|
+
}
|
|
14840
|
+
/**
|
|
14841
|
+
* Matches a prop to a row by name and namespace. An absent ns is NIST's default
|
|
14842
|
+
* namespace, except that for a legacy row it is also accepted as the row's own prop.
|
|
14843
|
+
*/
|
|
14844
|
+
function matchVocabularyProp(row, index, p) {
|
|
14845
|
+
if (p.name !== row.name) return void 0;
|
|
14846
|
+
const ns = p.ns ?? "";
|
|
14847
|
+
let legacy = false;
|
|
14848
|
+
if ((ns === "" ? table.defaultNamespace : ns) !== row.ns) {
|
|
14849
|
+
if (ns !== "" || !row.legacy) return void 0;
|
|
14850
|
+
legacy = true;
|
|
14851
|
+
}
|
|
14852
|
+
return {
|
|
14853
|
+
index,
|
|
14854
|
+
value: row.ns === table.namespace && p.remarks ? p.remarks : p.value,
|
|
14855
|
+
legacy
|
|
14856
|
+
};
|
|
14857
|
+
}
|
|
14858
|
+
/** The first prop in props that is the named row's prop. Mirrors FindVocabularyProp in Go. */
|
|
14859
|
+
function findVocabularyProp(props, name) {
|
|
14860
|
+
return findVocabularyProps(props, name)[0];
|
|
14861
|
+
}
|
|
14862
|
+
/** Every prop in props that is the named row's prop, in order. Mirrors FindVocabularyProps in Go. */
|
|
14863
|
+
function findVocabularyProps(props, name) {
|
|
14864
|
+
const row = byName.get(name);
|
|
14865
|
+
if (!row || !props) return [];
|
|
14866
|
+
const matches = [];
|
|
14867
|
+
props.forEach((p, i) => {
|
|
14868
|
+
const m = matchVocabularyProp(row, i, p);
|
|
14869
|
+
if (m) matches.push(m);
|
|
14870
|
+
});
|
|
14871
|
+
return matches;
|
|
14872
|
+
}
|
|
14873
|
+
/**
|
|
14874
|
+
* The first prop in props that is the named row's prop and carries group; an
|
|
14875
|
+
* empty group matches only ungrouped props. Mirrors FindGroupedVocabularyProp in Go.
|
|
14876
|
+
*/
|
|
14877
|
+
function findGroupedVocabularyProp(props, name, group) {
|
|
14878
|
+
return findVocabularyProps(props, name).find((m) => (props[m.index].group ?? "") === group);
|
|
14879
|
+
}
|
|
14880
|
+
/**
|
|
14881
|
+
* Whether props carry the marker prop (empty-field or absent-field) naming field
|
|
14882
|
+
* within group (§1.7.3, §1.7.4). Mirrors HasFieldMarker in Go.
|
|
14883
|
+
*/
|
|
14884
|
+
function hasFieldMarker(props, marker, field, group) {
|
|
14885
|
+
return findVocabularyProps(props, marker).some((m) => m.value === field && (props[m.index].group ?? "") === group);
|
|
14886
|
+
}
|
|
14887
|
+
/**
|
|
14888
|
+
* Reads an optional HDF string field: the named row's prop value when present, an
|
|
14889
|
+
* empty string when empty-field names field (§1.7.3), and undefined when the field
|
|
14890
|
+
* is absent. Mirrors VocabularyString in Go.
|
|
14891
|
+
*/
|
|
14892
|
+
function vocabularyString(props, name, field, group) {
|
|
14893
|
+
const match = findGroupedVocabularyProp(props, name, group);
|
|
14894
|
+
if (match) return match.value;
|
|
14895
|
+
return hasFieldMarker(props, "empty-field", field, group) ? "" : void 0;
|
|
14896
|
+
}
|
|
14897
|
+
/**
|
|
14898
|
+
* Whether p is HDF's own and so must never be carried as a foreign prop: every
|
|
14899
|
+
* prop in the HDF namespace, and a prop with no ns whose name is a legacy row
|
|
14900
|
+
* (§1.4, §3.1). Mirrors ConsumedVocabularyProp in Go.
|
|
14901
|
+
*/
|
|
14902
|
+
function consumedVocabularyProp(p) {
|
|
14903
|
+
if (p.ns === table.namespace) return true;
|
|
14904
|
+
return (p.ns ?? "") === "" && byName.get(p.name)?.legacy === true;
|
|
14905
|
+
}
|
|
14906
|
+
//#endregion
|
|
13993
14907
|
//#region converters/oscal-to-hdf/typescript/shared.ts
|
|
13994
14908
|
/**
|
|
13995
14909
|
* Shared helper functions for OSCAL-to-HDF converters.
|
|
@@ -14023,7 +14937,7 @@ function controlIdsToNistTags(ids) {
|
|
|
14023
14937
|
return tags;
|
|
14024
14938
|
}
|
|
14025
14939
|
/**
|
|
14026
|
-
* Extracts the base control ID from
|
|
14940
|
+
* Extracts the base control ID from an assessment-plan objective ID.
|
|
14027
14941
|
* "ac-1.a.1_obj.1" -> "ac-1"
|
|
14028
14942
|
*/
|
|
14029
14943
|
function extractControlIdFromObjectiveId(objectiveId) {
|
|
@@ -14111,28 +15025,96 @@ function extractMetadata(m) {
|
|
|
14111
15025
|
lastModified: String(m["last-modified"])
|
|
14112
15026
|
};
|
|
14113
15027
|
}
|
|
14114
|
-
/** Matches NIST 800-53 tags with enhancements like "AC-2 (3)". */
|
|
14115
|
-
const nistEnhancementReverseRe = /^([A-Z]{2}-\d+)\s*\((\d+)\)$/;
|
|
14116
15028
|
/**
|
|
14117
|
-
*
|
|
14118
|
-
*
|
|
15029
|
+
* Returns the canonical OSCAL id of the NIST control a target-id names, when the
|
|
15030
|
+
* target, ignoring ASCII letter case, is a control ("ac-2", "ac-2.3"), optionally
|
|
15031
|
+
* followed by dot-separated parts and an objective or statement suffix
|
|
15032
|
+
* ("ac-2.3_obj.a", "au-1_smt.a", "ac-1.a.1_obj.1"), and NIST defines that control
|
|
15033
|
+
* at any supported revision; otherwise undefined, including for a target shaped
|
|
15034
|
+
* like a control NIST does not define. Mirrors Go's ConfirmedControlID.
|
|
14119
15035
|
*/
|
|
14120
|
-
function
|
|
14121
|
-
|
|
14122
|
-
const
|
|
14123
|
-
if (
|
|
14124
|
-
|
|
15036
|
+
function confirmedControlId(targetId) {
|
|
15037
|
+
const target = asciiLower(targetId);
|
|
15038
|
+
const controlId = objectiveIDRe.exec(target)?.[0];
|
|
15039
|
+
if (controlId === void 0 || !isObjectiveOrStatementSuffix(target.slice(controlId.length))) return;
|
|
15040
|
+
const tag = controlIdToNistTag(controlId);
|
|
15041
|
+
return SUPPORTED_NIST_REVISIONS.some((rev) => nistExists(tag, rev)) ? nistTagToControlId(tag) : void 0;
|
|
15042
|
+
}
|
|
15043
|
+
/** Lowercases only ASCII letters, so Go and TypeScript fold identically. */
|
|
15044
|
+
function asciiLower(s) {
|
|
15045
|
+
return s.replace(/[A-Z]/g, (c) => c.toLowerCase());
|
|
14125
15046
|
}
|
|
14126
15047
|
/**
|
|
14127
|
-
*
|
|
14128
|
-
*
|
|
15048
|
+
* Whether rest, what follows a control id in a lowercased target-id, is empty or
|
|
15049
|
+
* names an objective or statement of that control: optional dot-separated
|
|
15050
|
+
* letter-and-digit parts, then "_obj" or "_smt", alone or followed by non-empty
|
|
15051
|
+
* dot-separated parts.
|
|
14129
15052
|
*/
|
|
14130
|
-
function
|
|
14131
|
-
|
|
15053
|
+
function isObjectiveOrStatementSuffix(rest) {
|
|
15054
|
+
if (rest === "") return true;
|
|
15055
|
+
const cut = rest.indexOf("_");
|
|
15056
|
+
if (cut < 0) return false;
|
|
15057
|
+
const parts = rest.slice(0, cut);
|
|
15058
|
+
if (parts !== "") {
|
|
15059
|
+
if (!parts.startsWith(".")) return false;
|
|
15060
|
+
if (parts.slice(1).split(".").some((part) => part === "" || !isLowerAlphanumeric(part))) return false;
|
|
15061
|
+
}
|
|
15062
|
+
const [kind, ...after] = rest.slice(cut + 1).split(".");
|
|
15063
|
+
return (kind === "obj" || kind === "smt") && !after.includes("");
|
|
15064
|
+
}
|
|
15065
|
+
function isLowerAlphanumeric(part) {
|
|
15066
|
+
return [...part].every((c) => c >= "a" && c <= "z" || c >= "0" && c <= "9");
|
|
14132
15067
|
}
|
|
14133
15068
|
/**
|
|
14134
|
-
*
|
|
14135
|
-
*
|
|
15069
|
+
* Converts NIST 800-53 notation, in any spelling normalizeNistId accepts, to the
|
|
15070
|
+
* OSCAL id of the control it names; a statement part names its control. Anything
|
|
15071
|
+
* else is returned trimmed and lowercased.
|
|
15072
|
+
* "AC-1" -> "ac-1", "ac-2 (3)" -> "ac-2.3", "AC-8 c 1" -> "ac-8"
|
|
15073
|
+
*/
|
|
15074
|
+
function nistTagToControlId(tag) {
|
|
15075
|
+
return nistTagToControlRef(tag).controlId;
|
|
15076
|
+
}
|
|
15077
|
+
/**
|
|
15078
|
+
* Converts NIST 800-53 notation to the OSCAL control id and, for a statement
|
|
15079
|
+
* part, the OSCAL statement id ("AC-8 c 1" -> "ac-8", "ac-8_smt.c.1";
|
|
15080
|
+
* "AC-2 (3) (a)" -> "ac-2.3", "ac-2.3_smt.a"). statementId is empty when the tag
|
|
15081
|
+
* names a whole control. A tag that is not a NIST spelling is returned trimmed and
|
|
15082
|
+
* lowercased as controlId. Mirrors Go's NistTagToControlRef.
|
|
15083
|
+
*/
|
|
15084
|
+
function nistTagToControlRef(tag) {
|
|
15085
|
+
const trimmed = tag.trim();
|
|
15086
|
+
const normalized = normalizeNistId(trimmed.split(/\s+/).join(" "));
|
|
15087
|
+
if (normalized === void 0) return {
|
|
15088
|
+
controlId: trimmed.toLowerCase(),
|
|
15089
|
+
statementId: ""
|
|
15090
|
+
};
|
|
15091
|
+
const [head, ...parts] = normalized.split(" ");
|
|
15092
|
+
const [family, number] = head.split("-");
|
|
15093
|
+
let controlId = `${family.toLowerCase()}-${unpadNistNumber(number)}`;
|
|
15094
|
+
if (parts.length > 0 && parts[0][0] >= "0" && parts[0][0] <= "9") controlId += `.${unpadNistNumber(parts.shift())}`;
|
|
15095
|
+
if (parts.length === 0) return {
|
|
15096
|
+
controlId,
|
|
15097
|
+
statementId: ""
|
|
15098
|
+
};
|
|
15099
|
+
return {
|
|
15100
|
+
controlId,
|
|
15101
|
+
statementId: `${controlId}_smt.${parts.map(unpadNistNumber).join(".")}`
|
|
15102
|
+
};
|
|
15103
|
+
}
|
|
15104
|
+
/** Strips the zero normalizeNistId pads a one-digit number with. */
|
|
15105
|
+
function unpadNistNumber(part) {
|
|
15106
|
+
return part.length === 2 && part.startsWith("0") ? part.slice(1) : part;
|
|
15107
|
+
}
|
|
15108
|
+
/**
|
|
15109
|
+
* Converts a 0.0-1.0 impact value to an OSCAL severity string.
|
|
15110
|
+
* This is the reverse of extractRiskSeverity.
|
|
15111
|
+
*/
|
|
15112
|
+
function impactToSeverity$1(impact) {
|
|
15113
|
+
return oscalSeverityFromHdf(impactToSeverity(impact));
|
|
15114
|
+
}
|
|
15115
|
+
/**
|
|
15116
|
+
* Maps an HDF status string to an OSCAL risk status string.
|
|
15117
|
+
* "passed"/"notApplicable" -> "closed", everything else -> "open".
|
|
14136
15118
|
*/
|
|
14137
15119
|
function hdfStatusToOscalRiskStatus(status) {
|
|
14138
15120
|
if (status === "passed" || status === "notApplicable") return "closed";
|
|
@@ -14141,6 +15123,22 @@ function hdfStatusToOscalRiskStatus(status) {
|
|
|
14141
15123
|
/** OSCAL specification version used in reverse converter output documents. */
|
|
14142
15124
|
const OSCAL_VERSION = "1.1.2";
|
|
14143
15125
|
/**
|
|
15126
|
+
* Builds the description-label prop that marks an OSCAL prose home with the HDF
|
|
15127
|
+
* description label whose text it carries.
|
|
15128
|
+
*/
|
|
15129
|
+
function descriptionLabelProp(label) {
|
|
15130
|
+
const prop = vocabularyProp("description-label", label);
|
|
15131
|
+
if (!prop) throw new Error(`oscal: description label ${JSON.stringify(label)} yields no description-label prop`);
|
|
15132
|
+
return prop;
|
|
15133
|
+
}
|
|
15134
|
+
/**
|
|
15135
|
+
* Returns props' description-label, or '' when there is none; a
|
|
15136
|
+
* description-label in any other namespace is foreign.
|
|
15137
|
+
*/
|
|
15138
|
+
function descriptionLabel(props) {
|
|
15139
|
+
return findVocabularyProp(props, "description-label")?.value ?? "";
|
|
15140
|
+
}
|
|
15141
|
+
/**
|
|
14144
15142
|
* Converts a title to kebab-case, truncated to 80 characters.
|
|
14145
15143
|
* Shared utility used by multiple converters for deriving names.
|
|
14146
15144
|
*/
|
|
@@ -14185,8 +15183,7 @@ function toKebabCase(title, fallback) {
|
|
|
14185
15183
|
*
|
|
14186
15184
|
* Two different ids can encode to the same token ('a/b' and 'a:b' both yield
|
|
14187
15185
|
* 'a_b'), which is why callers must also record the source id in the emitted
|
|
14188
|
-
* document — for SAR that is
|
|
14189
|
-
* StringDatatype forbids a padded value.
|
|
15186
|
+
* document — for SAR that is the finding's hdf-requirement-id prop.
|
|
14190
15187
|
*/
|
|
14191
15188
|
function oscalToken(s) {
|
|
14192
15189
|
if (s === "") return "";
|
|
@@ -14220,6 +15217,33 @@ function oscalString(s) {
|
|
|
14220
15217
|
* Results JSON and produces an OSCAL 1.1.2 assessment-results JSON document.
|
|
14221
15218
|
*/
|
|
14222
15219
|
/**
|
|
15220
|
+
* Adds a control, or one statement of it, to the reviewed-controls selection, one
|
|
15221
|
+
* entry per control. A control selected whole carries no statement-ids, because
|
|
15222
|
+
* listing any would narrow the selection to them. Mirrors Go's selectControl.
|
|
15223
|
+
*/
|
|
15224
|
+
function selectControl(controls, index, controlId, statementId) {
|
|
15225
|
+
const i = index.get(controlId);
|
|
15226
|
+
if (i === void 0) {
|
|
15227
|
+
index.set(controlId, controls.length);
|
|
15228
|
+
controls.push(statementId === "" ? { "control-id": controlId } : {
|
|
15229
|
+
"control-id": controlId,
|
|
15230
|
+
"statement-ids": [statementId]
|
|
15231
|
+
});
|
|
15232
|
+
return;
|
|
15233
|
+
}
|
|
15234
|
+
const selection = controls[i];
|
|
15235
|
+
const statementIds = selection["statement-ids"];
|
|
15236
|
+
if (statementIds === void 0) return;
|
|
15237
|
+
if (statementId === "") delete selection["statement-ids"];
|
|
15238
|
+
else if (!statementIds.includes(statementId)) statementIds.push(statementId);
|
|
15239
|
+
}
|
|
15240
|
+
/**
|
|
15241
|
+
* Accompanies the include-all selection of a result whose baseline names no
|
|
15242
|
+
* control, so the selection is not read as a claim. Mirrors Go's
|
|
15243
|
+
* noIdentifiableControlsRemark.
|
|
15244
|
+
*/
|
|
15245
|
+
const NO_IDENTIFIABLE_CONTROLS_REMARK = "No controls were identifiable in the assessed input, so none is listed individually. OSCAL requires a control selection; include-all is emitted to satisfy it and does not assert that any control was assessed.";
|
|
15246
|
+
/**
|
|
14223
15247
|
* Convert HDF Results JSON to OSCAL Assessment Results (SAR) JSON.
|
|
14224
15248
|
*
|
|
14225
15249
|
* @param input - HDF Results JSON string
|
|
@@ -14319,18 +15343,15 @@ function baselineToResult(baseline, timestamp, toolActorUuid, subjects) {
|
|
|
14319
15343
|
let description = "Converted from HDF results";
|
|
14320
15344
|
if (baseline.description && baseline.description !== "") description = baseline.description;
|
|
14321
15345
|
const resultProps = [];
|
|
14322
|
-
if (
|
|
14323
|
-
name: "baseline-version",
|
|
14324
|
-
value: baseline.version
|
|
14325
|
-
});
|
|
15346
|
+
if (typeof baseline.version === "string") pushVocabularyProp(resultProps, "baseline-version", baseline.version);
|
|
14326
15347
|
const findings = [];
|
|
14327
15348
|
const observations = [];
|
|
14328
15349
|
const risks = [];
|
|
14329
15350
|
const resources = [];
|
|
14330
15351
|
const includeControls = [];
|
|
14331
|
-
const
|
|
15352
|
+
const controlIndex = /* @__PURE__ */ new Map();
|
|
14332
15353
|
for (const req of baseline.requirements) {
|
|
14333
|
-
const nistId =
|
|
15354
|
+
const { controlId: nistId, statementId } = nistTagToControlRef(req.id ?? "");
|
|
14334
15355
|
if (nistId === "") continue;
|
|
14335
15356
|
const { finding, observation, risk, resource } = requirementToFindingSet(req, timestamp, toolActorUuid, subjects);
|
|
14336
15357
|
findings.push(finding);
|
|
@@ -14338,12 +15359,12 @@ function baselineToResult(baseline, timestamp, toolActorUuid, subjects) {
|
|
|
14338
15359
|
if (risk) risks.push(risk);
|
|
14339
15360
|
if (resource) resources.push(resource);
|
|
14340
15361
|
const cid = oscalToken(nistId);
|
|
14341
|
-
if (cid !== ""
|
|
14342
|
-
seenControl.add(cid);
|
|
14343
|
-
includeControls.push({ "control-id": cid });
|
|
14344
|
-
}
|
|
15362
|
+
if (cid !== "") selectControl(includeControls, controlIndex, cid, statementId);
|
|
14345
15363
|
}
|
|
14346
|
-
const controlSelection = includeControls.length > 0 ? { "include-controls": includeControls } : {
|
|
15364
|
+
const controlSelection = includeControls.length > 0 ? { "include-controls": includeControls } : {
|
|
15365
|
+
"include-all": {},
|
|
15366
|
+
remarks: NO_IDENTIFIABLE_CONTROLS_REMARK
|
|
15367
|
+
};
|
|
14347
15368
|
return {
|
|
14348
15369
|
result: {
|
|
14349
15370
|
uuid: crypto.randomUUID(),
|
|
@@ -14384,31 +15405,17 @@ function buildSubjects(components) {
|
|
|
14384
15405
|
* Converts an EvaluatedRequirement into a Finding, optional Observation, and optional Risk.
|
|
14385
15406
|
*/
|
|
14386
15407
|
function requirementToFindingSet(req, timestamp, toolActorUuid, subjects) {
|
|
14387
|
-
const
|
|
15408
|
+
const { controlId, statementId } = nistTagToControlRef(req.id ?? "");
|
|
15409
|
+
const [targetType, targetId] = statementId === "" ? ["objective-id", oscalToken(controlId)] : ["statement-id", statementId];
|
|
14388
15410
|
const results = req.results ?? [];
|
|
14389
15411
|
const descriptions = req.descriptions ?? [];
|
|
14390
15412
|
const { state, reason } = effectiveState(req);
|
|
14391
15413
|
const findingDesc = extractDefaultDescription(descriptions);
|
|
14392
|
-
const props = [
|
|
14393
|
-
name: "hdf-requirement-id",
|
|
14394
|
-
value: oscalString(req.id)
|
|
14395
|
-
}];
|
|
15414
|
+
const props = [];
|
|
14396
15415
|
const addProp = (name, value) => {
|
|
14397
|
-
|
|
14398
|
-
name,
|
|
14399
|
-
value
|
|
14400
|
-
});
|
|
14401
|
-
};
|
|
14402
|
-
const addProseProp = (name, text) => {
|
|
14403
|
-
const preview = previewLine(text);
|
|
14404
|
-
if (preview === "") return;
|
|
14405
|
-
const p = {
|
|
14406
|
-
name,
|
|
14407
|
-
value: preview
|
|
14408
|
-
};
|
|
14409
|
-
if (preview !== text) p.remarks = text;
|
|
14410
|
-
props.push(p);
|
|
15416
|
+
pushVocabularyProp(props, name, value);
|
|
14411
15417
|
};
|
|
15418
|
+
addProp("hdf-requirement-id", req.id);
|
|
14412
15419
|
const descriptionByLabel = (label) => {
|
|
14413
15420
|
const d = descriptions.find((x) => x.label === label);
|
|
14414
15421
|
return d ? d.data : "";
|
|
@@ -14421,9 +15428,6 @@ function requirementToFindingSet(req, timestamp, toolActorUuid, subjects) {
|
|
|
14421
15428
|
};
|
|
14422
15429
|
pushTagValues("nist");
|
|
14423
15430
|
pushTagValues("cci");
|
|
14424
|
-
addProseProp("check", descriptionByLabel("check"));
|
|
14425
|
-
addProseProp("rationale", descriptionByLabel("rationale"));
|
|
14426
|
-
if (req.impact <= 0) addProseProp("fix", descriptionByLabel("fix"));
|
|
14427
15431
|
if (req.controlType) addProp("control-type", req.controlType);
|
|
14428
15432
|
if (req.verificationMethod) addProp("verification-method", req.verificationMethod);
|
|
14429
15433
|
if (req.applicability) addProp("applicability", req.applicability);
|
|
@@ -14451,7 +15455,7 @@ function requirementToFindingSet(req, timestamp, toolActorUuid, subjects) {
|
|
|
14451
15455
|
href: o.uri,
|
|
14452
15456
|
rel: "reference"
|
|
14453
15457
|
});
|
|
14454
|
-
else if (typeof o.ref === "string")
|
|
15458
|
+
else if (typeof o.ref === "string") addProp("reference", o.ref);
|
|
14455
15459
|
}
|
|
14456
15460
|
for (const er of req.externalReferences ?? []) if (typeof er.href === "string" && er.href !== "") links.push({
|
|
14457
15461
|
href: er.href,
|
|
@@ -14465,6 +15469,7 @@ function requirementToFindingSet(req, timestamp, toolActorUuid, subjects) {
|
|
|
14465
15469
|
resource = {
|
|
14466
15470
|
uuid: resourceUuid,
|
|
14467
15471
|
title: `Check source code for ${req.id}`,
|
|
15472
|
+
props: [vocabularyProp("type", "evidence")],
|
|
14468
15473
|
base64: {
|
|
14469
15474
|
value: encodeBase64Utf8(req.code),
|
|
14470
15475
|
"media-type": "text/plain"
|
|
@@ -14479,9 +15484,11 @@ function requirementToFindingSet(req, timestamp, toolActorUuid, subjects) {
|
|
|
14479
15484
|
if (reason) targetStatus.reason = reason;
|
|
14480
15485
|
const remarks = overrideRemarks(req);
|
|
14481
15486
|
if (remarks) targetStatus.remarks = remarks;
|
|
15487
|
+
const rationale = descriptionByLabel("rationale");
|
|
14482
15488
|
const target = {
|
|
14483
|
-
type:
|
|
14484
|
-
"target-id":
|
|
15489
|
+
type: targetType,
|
|
15490
|
+
"target-id": targetId,
|
|
15491
|
+
...rationale !== "" ? { description: rationale } : {},
|
|
14485
15492
|
status: targetStatus
|
|
14486
15493
|
};
|
|
14487
15494
|
const finding = {
|
|
@@ -14506,7 +15513,7 @@ function requirementToFindingSet(req, timestamp, toolActorUuid, subjects) {
|
|
|
14506
15513
|
...relevantEvidence.length > 0 ? { "relevant-evidence": relevantEvidence } : {}
|
|
14507
15514
|
};
|
|
14508
15515
|
finding["related-observations"] = [{ "observation-uuid": obsUUID }];
|
|
14509
|
-
}
|
|
15516
|
+
} else warnUncarriedProse(req);
|
|
14510
15517
|
let risk;
|
|
14511
15518
|
if (req.impact > 0) {
|
|
14512
15519
|
const riskUUID = crypto.randomUUID();
|
|
@@ -14548,8 +15555,8 @@ function requirementToFindingSet(req, timestamp, toolActorUuid, subjects) {
|
|
|
14548
15555
|
};
|
|
14549
15556
|
}
|
|
14550
15557
|
/**
|
|
14551
|
-
* Reduces prose to a single line
|
|
14552
|
-
*
|
|
15558
|
+
* Reduces prose to a single line for an OSCAL single-line field: the first
|
|
15559
|
+
* non-empty line, trimmed, truncated to 120 code points.
|
|
14553
15560
|
*/
|
|
14554
15561
|
function previewLine(text) {
|
|
14555
15562
|
for (let line of text.split("\n")) {
|
|
@@ -14557,7 +15564,7 @@ function previewLine(text) {
|
|
|
14557
15564
|
if (line === "") continue;
|
|
14558
15565
|
const maxRunes = 120;
|
|
14559
15566
|
const runes = Array.from(line);
|
|
14560
|
-
if (runes.length > maxRunes) line = `${runes.slice(0,
|
|
15567
|
+
if (runes.length > maxRunes) line = `${runes.slice(0, 117).join("").trim()}...`;
|
|
14561
15568
|
return line;
|
|
14562
15569
|
}
|
|
14563
15570
|
return "";
|
|
@@ -14612,9 +15619,10 @@ function overrideRemarks(req) {
|
|
|
14612
15619
|
return parts.join("; ");
|
|
14613
15620
|
}
|
|
14614
15621
|
/**
|
|
14615
|
-
* Collects the requirement's refs, evidence
|
|
14616
|
-
* observation relevant-evidence — the home
|
|
14617
|
-
* into HDF refs (via href)
|
|
15622
|
+
* Collects the requirement's refs, evidence and source location, then its check
|
|
15623
|
+
* (and impact-0 fix) prose, into OSCAL observation relevant-evidence — the home
|
|
15624
|
+
* the reverse SAR importer reads back into HDF refs (via href), evidence (via
|
|
15625
|
+
* description) and check/fix (via description-label).
|
|
14618
15626
|
*/
|
|
14619
15627
|
function buildRelevantEvidence(req) {
|
|
14620
15628
|
const ev = [];
|
|
@@ -14638,8 +15646,37 @@ function buildRelevantEvidence(req) {
|
|
|
14638
15646
|
const loc = sourceLocationText(req.sourceLocation);
|
|
14639
15647
|
if (loc) ev.push({ description: "Source location: " + loc });
|
|
14640
15648
|
}
|
|
15649
|
+
for (const label of observationProseLabels(req)) {
|
|
15650
|
+
const text = descriptionText(req, label);
|
|
15651
|
+
ev.push({
|
|
15652
|
+
description: previewLine(text),
|
|
15653
|
+
props: [descriptionLabelProp(label)],
|
|
15654
|
+
remarks: text
|
|
15655
|
+
});
|
|
15656
|
+
}
|
|
14641
15657
|
return ev;
|
|
14642
15658
|
}
|
|
15659
|
+
/** Data of the requirement's first description with the label, or ''. */
|
|
15660
|
+
function descriptionText(req, label) {
|
|
15661
|
+
return (req.descriptions ?? []).find((d) => d.label === label)?.data ?? "";
|
|
15662
|
+
}
|
|
15663
|
+
/**
|
|
15664
|
+
* Description labels whose text the requirement's observation carries as
|
|
15665
|
+
* labelled evidence: check, and fix when impact is 0 (otherwise fix's home is
|
|
15666
|
+
* the risk remediation). A description with no preview text carries nothing.
|
|
15667
|
+
*/
|
|
15668
|
+
function observationProseLabels(req) {
|
|
15669
|
+
return (req.impact <= 0 ? ["check", "fix"] : ["check"]).filter((label) => previewLine(descriptionText(req, label)) !== "");
|
|
15670
|
+
}
|
|
15671
|
+
/**
|
|
15672
|
+
* Reports observation-scoped prose lost because a requirement with no results
|
|
15673
|
+
* emits no observation.
|
|
15674
|
+
*/
|
|
15675
|
+
function warnUncarriedProse(req) {
|
|
15676
|
+
const labels = observationProseLabels(req);
|
|
15677
|
+
if (labels.length === 1) emitConverterWarning(`hdf-to-oscal-sar: requirement ${JSON.stringify(req.id)} has no results, so no observation holds its ${labels[0]} description; it was not carried`);
|
|
15678
|
+
else if (labels.length > 1) emitConverterWarning(`hdf-to-oscal-sar: requirement ${JSON.stringify(req.id)} has no results, so no observation holds its ${labels.join(" and ")} descriptions; they were not carried`);
|
|
15679
|
+
}
|
|
14643
15680
|
/** Renders a source location as "ref:line", degrading to whichever is present. */
|
|
14644
15681
|
function sourceLocationText(loc) {
|
|
14645
15682
|
const ref = loc.ref ?? "";
|
|
@@ -14652,8 +15689,9 @@ function severityToFacetValue(s) {
|
|
|
14652
15689
|
}
|
|
14653
15690
|
/**
|
|
14654
15691
|
* Turns the requirement's fix description and any governing risk-acceptance
|
|
14655
|
-
* override into OSCAL risk remediations
|
|
14656
|
-
* back as the HDF
|
|
15692
|
+
* override into OSCAL risk remediations. The reverse importer reads the
|
|
15693
|
+
* labelled fix back as the HDF fix description and the rest as the remediation
|
|
15694
|
+
* description.
|
|
14657
15695
|
*/
|
|
14658
15696
|
function buildRemediations(req) {
|
|
14659
15697
|
const rems = [];
|
|
@@ -14662,7 +15700,8 @@ function buildRemediations(req) {
|
|
|
14662
15700
|
uuid: crypto.randomUUID(),
|
|
14663
15701
|
lifecycle: "recommendation",
|
|
14664
15702
|
title: "Recommended fix",
|
|
14665
|
-
description: fix.data
|
|
15703
|
+
description: fix.data,
|
|
15704
|
+
props: [descriptionLabelProp("fix")]
|
|
14666
15705
|
});
|
|
14667
15706
|
const overrides = req.statusOverrides ?? [];
|
|
14668
15707
|
if (req.disposition && overrides.length > 0) {
|
|
@@ -14726,6 +15765,13 @@ function riskStatusFromState(state) {
|
|
|
14726
15765
|
* Converts HDF Amendments to OSCAL Plan of Action and Milestones (POA&M) format.
|
|
14727
15766
|
*
|
|
14728
15767
|
* This is the reverse direction of the oscal-poam to HDF converter.
|
|
15768
|
+
*
|
|
15769
|
+
* Every amendments field has an OSCAL home the importer reads back exactly
|
|
15770
|
+
* (ADR-0014 §4.6), with four exclusions: the document integrity and signature,
|
|
15771
|
+
* the generator (the importer stamps its own), and each override's signature and
|
|
15772
|
+
* previousChecksum. A signature covers the original HDF bytes and the checksum
|
|
15773
|
+
* chain covers document order; neither survives a format change, and the
|
|
15774
|
+
* importer re-chains the overrides it reads.
|
|
14729
15775
|
*/
|
|
14730
15776
|
/**
|
|
14731
15777
|
* Convert HDF Amendments JSON to OSCAL POA&M JSON.
|
|
@@ -14738,6 +15784,12 @@ async function convertHdfToOscalPoam(input) {
|
|
|
14738
15784
|
const doc = { "plan-of-action-and-milestones": amendmentsToPOAM(amendmentsDoc) };
|
|
14739
15785
|
return JSON.stringify(doc, null, 2);
|
|
14740
15786
|
}
|
|
15787
|
+
/** Role ids of the metadata roles the exporter defines. */
|
|
15788
|
+
const ROLE_PREPARED_BY = "prepared-by";
|
|
15789
|
+
const ROLE_APPROVED_BY = "approved-by";
|
|
15790
|
+
const ROLE_COMPLETED_BY = "completed-by";
|
|
15791
|
+
/** Marks the risk-log entry whose start is the override's appliedAt and whose logged-by names its appliedBy. */
|
|
15792
|
+
const OVERRIDE_APPLIED_TITLE = "Override applied";
|
|
14741
15793
|
/**
|
|
14742
15794
|
* Picks the document title, which OSCAL requires on metadata. The HDF name is
|
|
14743
15795
|
* schema-required, so the fallbacks only matter for a document that slipped
|
|
@@ -14747,13 +15799,25 @@ function poamTitle(a) {
|
|
|
14747
15799
|
return firstNonEmpty(a.name, a.amendmentId ?? "", "HDF Amendments");
|
|
14748
15800
|
}
|
|
14749
15801
|
/**
|
|
15802
|
+
* Stands in for an empty requirementId: HDF puts no minLength on it, and OSCAL
|
|
15803
|
+
* 1.2.x requires risk and POA&M item titles to be non-empty. It is display text
|
|
15804
|
+
* only; the requirement id rides in hdf-requirement-id. Mirrors Go's
|
|
15805
|
+
* unidentifiedRequirementTitle.
|
|
15806
|
+
*/
|
|
15807
|
+
const UNIDENTIFIED_REQUIREMENT_TITLE = "Unidentified requirement";
|
|
15808
|
+
/** The title of the risk and POA&M item an override produces. */
|
|
15809
|
+
function requirementTitle(override) {
|
|
15810
|
+
return firstNonEmpty(override.requirementId, UNIDENTIFIED_REQUIREMENT_TITLE);
|
|
15811
|
+
}
|
|
15812
|
+
/**
|
|
14750
15813
|
* Supplies the text OSCAL requires for a risk's description and statement. HDF
|
|
14751
15814
|
* puts no minLength on reason, so an override can legitimately carry none; the
|
|
14752
15815
|
* fallback states that absence rather than inventing an impact assessment the
|
|
14753
|
-
* source never made.
|
|
15816
|
+
* source never made, and names the requirement only when there is one.
|
|
14754
15817
|
*/
|
|
14755
15818
|
function riskRationale(override) {
|
|
14756
|
-
|
|
15819
|
+
const absence = firstNonEmpty(override.requirementId) === "" ? `No rationale was recorded for the ${String(override.type)} override.` : `No rationale was recorded for the ${String(override.type)} override applied to ${override.requirementId}.`;
|
|
15820
|
+
return firstNonEmpty(override.reason, absence);
|
|
14757
15821
|
}
|
|
14758
15822
|
/** HDF dates arrive as strings from JSON.parse but are typed as Date. */
|
|
14759
15823
|
function toDate(value) {
|
|
@@ -14763,72 +15827,87 @@ function toDate(value) {
|
|
|
14763
15827
|
return d;
|
|
14764
15828
|
}
|
|
14765
15829
|
/**
|
|
14766
|
-
* Deduplicates HDF identities into OSCAL metadata parties
|
|
14767
|
-
*
|
|
14768
|
-
*
|
|
15830
|
+
* Deduplicates HDF identities into OSCAL metadata parties, one per distinct
|
|
15831
|
+
* (identifier, type, description) triple; an absent and an empty description are
|
|
15832
|
+
* distinct. Insertion order is preserved for deterministic output.
|
|
14769
15833
|
*/
|
|
14770
15834
|
var PartyRegistry = class {
|
|
14771
15835
|
constructor() {
|
|
14772
|
-
this.
|
|
15836
|
+
this.byKey = /* @__PURE__ */ new Map();
|
|
14773
15837
|
}
|
|
14774
15838
|
getOrAdd(id) {
|
|
14775
|
-
const
|
|
14776
|
-
|
|
15839
|
+
const key = JSON.stringify([
|
|
15840
|
+
id.identifier,
|
|
15841
|
+
id.type,
|
|
15842
|
+
id.description ?? null
|
|
15843
|
+
]);
|
|
15844
|
+
const existing = this.byKey.get(key);
|
|
14777
15845
|
if (existing) return existing.uuid;
|
|
15846
|
+
const name = oscalString(id.identifier);
|
|
15847
|
+
const props = [];
|
|
15848
|
+
pushVocabularyProp(props, "identity-identifier", id.identifier ?? "");
|
|
15849
|
+
pushVocabularyProp(props, "identity-type", String(id.type));
|
|
15850
|
+
if (id.description === "") props.push(emptyFieldProp("description"));
|
|
14778
15851
|
const party = {
|
|
14779
15852
|
uuid: crypto.randomUUID(),
|
|
14780
15853
|
type: "person",
|
|
14781
|
-
...name === "" ? {} : { name }
|
|
15854
|
+
...name === "" ? {} : { name },
|
|
15855
|
+
...props.length > 0 ? { props } : {},
|
|
15856
|
+
...id.description ? { remarks: id.description } : {}
|
|
14782
15857
|
};
|
|
14783
|
-
this.
|
|
15858
|
+
this.byKey.set(key, party);
|
|
14784
15859
|
return party.uuid;
|
|
14785
15860
|
}
|
|
14786
15861
|
list() {
|
|
14787
|
-
return [...this.
|
|
15862
|
+
return [...this.byKey.values()];
|
|
14788
15863
|
}
|
|
14789
15864
|
};
|
|
14790
15865
|
/**
|
|
14791
15866
|
* Converts parsed HDFAmendments to an OSCAL PlanOfActionAndMilestones.
|
|
14792
15867
|
*/
|
|
14793
15868
|
function amendmentsToPOAM(amendments) {
|
|
14794
|
-
const
|
|
15869
|
+
const b = {
|
|
15870
|
+
parties: new PartyRegistry(),
|
|
15871
|
+
observations: [],
|
|
15872
|
+
resources: [],
|
|
15873
|
+
completedBy: false
|
|
15874
|
+
};
|
|
14795
15875
|
const roles = [];
|
|
14796
15876
|
const responsibleParties = [];
|
|
14797
15877
|
if (amendments.appliedBy) {
|
|
14798
|
-
const uuid = parties.getOrAdd(amendments.appliedBy);
|
|
15878
|
+
const uuid = b.parties.getOrAdd(amendments.appliedBy);
|
|
14799
15879
|
roles.push({
|
|
14800
|
-
id:
|
|
15880
|
+
id: ROLE_PREPARED_BY,
|
|
14801
15881
|
title: "Prepared By"
|
|
14802
15882
|
});
|
|
14803
15883
|
responsibleParties.push({
|
|
14804
|
-
"role-id":
|
|
15884
|
+
"role-id": ROLE_PREPARED_BY,
|
|
14805
15885
|
"party-uuids": [uuid]
|
|
14806
15886
|
});
|
|
14807
15887
|
}
|
|
14808
15888
|
if (amendments.approvedBy) {
|
|
14809
|
-
const uuid = parties.getOrAdd(amendments.approvedBy);
|
|
15889
|
+
const uuid = b.parties.getOrAdd(amendments.approvedBy);
|
|
14810
15890
|
roles.push({
|
|
14811
|
-
id:
|
|
15891
|
+
id: ROLE_APPROVED_BY,
|
|
14812
15892
|
title: "Approved By"
|
|
14813
15893
|
});
|
|
14814
15894
|
responsibleParties.push({
|
|
14815
|
-
"role-id":
|
|
15895
|
+
"role-id": ROLE_APPROVED_BY,
|
|
14816
15896
|
"party-uuids": [uuid]
|
|
14817
15897
|
});
|
|
14818
15898
|
}
|
|
14819
|
-
for (const override of amendments.overrides) parties.getOrAdd(override.appliedBy);
|
|
14820
|
-
let importSSP;
|
|
14821
|
-
if (amendments.systemRef && amendments.systemRef !== "") importSSP = { href: amendments.systemRef };
|
|
14822
|
-
else importSSP = { href: "#" };
|
|
15899
|
+
for (const override of amendments.overrides) b.parties.getOrAdd(override.appliedBy);
|
|
14823
15900
|
const poamItems = [];
|
|
14824
15901
|
const risks = [];
|
|
14825
|
-
const observations = [];
|
|
14826
15902
|
for (const override of amendments.overrides) {
|
|
14827
|
-
const { item,
|
|
15903
|
+
const { item, risk } = overrideToPOAMItem(override, b);
|
|
14828
15904
|
poamItems.push(item);
|
|
14829
|
-
risks.push(
|
|
14830
|
-
observations.push(...itemObs);
|
|
15905
|
+
risks.push(risk);
|
|
14831
15906
|
}
|
|
15907
|
+
if (b.completedBy) roles.push({
|
|
15908
|
+
id: ROLE_COMPLETED_BY,
|
|
15909
|
+
title: "Completed By"
|
|
15910
|
+
});
|
|
14832
15911
|
const metadata = {
|
|
14833
15912
|
title: poamTitle(amendments),
|
|
14834
15913
|
"last-modified": latestAppliedAt(amendments.overrides),
|
|
@@ -14836,139 +15915,201 @@ function amendmentsToPOAM(amendments) {
|
|
|
14836
15915
|
"oscal-version": OSCAL_VERSION
|
|
14837
15916
|
};
|
|
14838
15917
|
if (amendments.description) metadata.remarks = amendments.description;
|
|
14839
|
-
const partyList = parties.list();
|
|
15918
|
+
const partyList = b.parties.list();
|
|
14840
15919
|
if (partyList.length > 0) metadata.parties = partyList;
|
|
14841
|
-
if (roles.length > 0)
|
|
14842
|
-
|
|
14843
|
-
metadata["responsible-parties"] = responsibleParties;
|
|
14844
|
-
}
|
|
15920
|
+
if (roles.length > 0) metadata.roles = roles;
|
|
15921
|
+
if (responsibleParties.length > 0) metadata["responsible-parties"] = responsibleParties;
|
|
14845
15922
|
const metaProps = metadataProps(amendments);
|
|
14846
15923
|
if (metaProps.length > 0) metadata.props = metaProps;
|
|
14847
15924
|
const poam = {
|
|
14848
15925
|
uuid: crypto.randomUUID(),
|
|
14849
15926
|
metadata,
|
|
14850
|
-
"import-ssp":
|
|
15927
|
+
"import-ssp": { href: amendments.systemRef ? amendments.systemRef : "#" },
|
|
14851
15928
|
"poam-items": poamItems
|
|
14852
15929
|
};
|
|
14853
15930
|
if (risks.length > 0) poam.risks = risks;
|
|
14854
|
-
if (observations.length > 0) poam.observations = observations;
|
|
14855
|
-
|
|
14856
|
-
if (resources.length > 0) poam["back-matter"] = { resources };
|
|
15931
|
+
if (b.observations.length > 0) poam.observations = b.observations;
|
|
15932
|
+
if (b.resources.length > 0) poam["back-matter"] = { resources: b.resources };
|
|
14857
15933
|
return poam;
|
|
14858
15934
|
}
|
|
14859
15935
|
/**
|
|
14860
|
-
*
|
|
14861
|
-
*
|
|
15936
|
+
* Carries an optional HDF string field: its prop when the value is non-empty,
|
|
15937
|
+
* empty-field when it is present and empty (§1.7.3), and nothing when it is
|
|
15938
|
+
* absent. Mirrors appendOptionalString in Go.
|
|
14862
15939
|
*/
|
|
14863
|
-
function
|
|
15940
|
+
function pushOptionalString(props, name, field, value) {
|
|
15941
|
+
if (value === void 0 || value === null) return;
|
|
15942
|
+
if (value === "") {
|
|
15943
|
+
props.push(emptyFieldProp(field));
|
|
15944
|
+
return;
|
|
15945
|
+
}
|
|
15946
|
+
pushVocabularyProp(props, name, value);
|
|
15947
|
+
}
|
|
15948
|
+
/** Renders HDF text as single-line OSCAL display text, falling back when there is none. */
|
|
15949
|
+
function displayLine(text, fallback) {
|
|
15950
|
+
return normalizePropValue(text) || fallback;
|
|
15951
|
+
}
|
|
15952
|
+
/** The HDF canonical trimmed-UTC form of a date, keeping its millisecond fraction. */
|
|
15953
|
+
function timestamp(value) {
|
|
15954
|
+
const d = toDate(value);
|
|
15955
|
+
return d ? formatTimestamp(d) : void 0;
|
|
15956
|
+
}
|
|
15957
|
+
/**
|
|
15958
|
+
* Converts a single StandaloneOverride to a POAMItem and its risk, adding its
|
|
15959
|
+
* evidence observations, back-matter resources and parties to the builder.
|
|
15960
|
+
*/
|
|
15961
|
+
function overrideToPOAMItem(override, b) {
|
|
14864
15962
|
const riskUUID = crypto.randomUUID();
|
|
14865
15963
|
const riskStatus = override.status ? hdfStatusToOscalRiskStatus(String(override.status)) : "open";
|
|
14866
|
-
const
|
|
14867
|
-
const
|
|
14868
|
-
|
|
14869
|
-
|
|
14870
|
-
|
|
14871
|
-
|
|
14872
|
-
|
|
14873
|
-
|
|
14874
|
-
|
|
14875
|
-
|
|
14876
|
-
});
|
|
14877
|
-
if (override.justification) pushStringProp(riskProps, "justification", String(override.justification));
|
|
14878
|
-
if (override.baselineRef) pushStringProp(riskProps, "baseline-ref", override.baselineRef);
|
|
14879
|
-
if (override.componentRef) pushStringProp(riskProps, "component-ref", override.componentRef);
|
|
14880
|
-
const remediations = [];
|
|
14881
|
-
if (override.milestones) for (const ms of override.milestones) {
|
|
14882
|
-
const msProps = [];
|
|
14883
|
-
if (ms.status) msProps.push({
|
|
14884
|
-
name: "milestone-status",
|
|
14885
|
-
value: String(ms.status)
|
|
14886
|
-
});
|
|
14887
|
-
let tasks;
|
|
14888
|
-
const d = ms.estimatedCompletion ? toDate(ms.estimatedCompletion) : void 0;
|
|
14889
|
-
if (d) {
|
|
14890
|
-
const eta = formatTimestampSeconds(d);
|
|
14891
|
-
const taskProps = milestoneCompletionProps(ms);
|
|
14892
|
-
tasks = [{
|
|
14893
|
-
uuid: crypto.randomUUID(),
|
|
14894
|
-
type: "milestone",
|
|
14895
|
-
title: ms.description,
|
|
14896
|
-
timing: { "within-date-range": {
|
|
14897
|
-
start: eta,
|
|
14898
|
-
end: eta
|
|
14899
|
-
} },
|
|
14900
|
-
...taskProps.length > 0 ? { props: taskProps } : {}
|
|
14901
|
-
}];
|
|
14902
|
-
}
|
|
14903
|
-
remediations.push({
|
|
14904
|
-
uuid: crypto.randomUUID(),
|
|
14905
|
-
lifecycle: "planned",
|
|
14906
|
-
title: ms.description,
|
|
14907
|
-
description: ms.description,
|
|
14908
|
-
props: msProps.length > 0 ? msProps : void 0,
|
|
14909
|
-
tasks
|
|
14910
|
-
});
|
|
15964
|
+
const applier = b.parties.getOrAdd(override.appliedBy);
|
|
15965
|
+
const remediations = milestoneRemediations(override.milestones ?? [], b);
|
|
15966
|
+
const characterizations = cvssCharacterizations(override.cvss, applier);
|
|
15967
|
+
const riskLog = riskLogFor(override, applier, riskStatus);
|
|
15968
|
+
const deadline = timestamp(override.expiresAt);
|
|
15969
|
+
const itemObs = [];
|
|
15970
|
+
for (const ev of override.evidence ?? []) {
|
|
15971
|
+
const obs = evidenceObservation(ev, override.appliedAt, b);
|
|
15972
|
+
b.observations.push(obs);
|
|
15973
|
+
itemObs.push(obs.uuid);
|
|
14911
15974
|
}
|
|
14912
|
-
const
|
|
14913
|
-
|
|
14914
|
-
const
|
|
14915
|
-
|
|
14916
|
-
|
|
14917
|
-
|
|
14918
|
-
|
|
14919
|
-
}] },
|
|
14920
|
-
facets: cvssFacets(override.cvss)
|
|
15975
|
+
const links = [];
|
|
15976
|
+
for (const ref of override.externalReferences ?? []) {
|
|
15977
|
+
const res = referenceResource(ref, b);
|
|
15978
|
+
b.resources.push(res);
|
|
15979
|
+
links.push({
|
|
15980
|
+
href: `#${res.uuid}`,
|
|
15981
|
+
rel: "reference"
|
|
14921
15982
|
});
|
|
14922
15983
|
}
|
|
14923
|
-
let riskLog;
|
|
14924
|
-
const expiresDate = override.expiresAt ? toDate(override.expiresAt) : void 0;
|
|
14925
|
-
if (expiresDate && expiresDate.getTime() > 0) riskLog = { entries: [{
|
|
14926
|
-
uuid: crypto.randomUUID(),
|
|
14927
|
-
title: "Scheduled review",
|
|
14928
|
-
description: "Amendment expiration date",
|
|
14929
|
-
start: formatTimestampSeconds(expiresDate),
|
|
14930
|
-
"status-change": riskStatus
|
|
14931
|
-
}] };
|
|
14932
|
-
const deadline = expiresDate && expiresDate.getTime() > 0 ? formatTimestampSeconds(expiresDate) : void 0;
|
|
14933
|
-
const itemObs = [];
|
|
14934
|
-
const relatedObs = [];
|
|
14935
|
-
const collected = observationCollected(override);
|
|
14936
|
-
if (override.evidence) for (const ev of override.evidence) {
|
|
14937
|
-
const obsUUID = crypto.randomUUID();
|
|
14938
|
-
itemObs.push(evidenceObservation(ev, obsUUID, collected));
|
|
14939
|
-
relatedObs.push({ "observation-uuid": obsUUID });
|
|
14940
|
-
}
|
|
14941
15984
|
const rationale = riskRationale(override);
|
|
14942
15985
|
const risk = {
|
|
14943
15986
|
uuid: riskUUID,
|
|
14944
|
-
title: override
|
|
15987
|
+
title: requirementTitle(override),
|
|
14945
15988
|
description: rationale,
|
|
14946
15989
|
statement: rationale,
|
|
15990
|
+
props: riskProps(override),
|
|
15991
|
+
...links.length > 0 ? { links } : {},
|
|
14947
15992
|
status: riskStatus,
|
|
14948
|
-
deadline,
|
|
14949
|
-
|
|
14950
|
-
|
|
14951
|
-
|
|
14952
|
-
"risk-log": riskLog
|
|
15993
|
+
...deadline ? { deadline } : {},
|
|
15994
|
+
...characterizations.length > 0 ? { characterizations } : {},
|
|
15995
|
+
...remediations.length > 0 ? { remediations } : {},
|
|
15996
|
+
...riskLog ? { "risk-log": riskLog } : {}
|
|
14953
15997
|
};
|
|
14954
15998
|
return {
|
|
14955
15999
|
item: {
|
|
14956
16000
|
uuid: crypto.randomUUID(),
|
|
14957
|
-
title: override
|
|
16001
|
+
title: requirementTitle(override),
|
|
14958
16002
|
description: override.reason,
|
|
14959
|
-
"related-
|
|
14960
|
-
|
|
16003
|
+
...itemObs.length > 0 ? { "related-observations": itemObs.map((uuid) => ({ "observation-uuid": uuid })) } : {},
|
|
16004
|
+
"related-risks": [{ "risk-uuid": riskUUID }]
|
|
14961
16005
|
},
|
|
14962
|
-
|
|
14963
|
-
itemObs
|
|
16006
|
+
risk
|
|
14964
16007
|
};
|
|
14965
16008
|
}
|
|
14966
16009
|
/**
|
|
16010
|
+
* Carries the override's identity, disposition and scope. The FedRAMP
|
|
16011
|
+
* impacted-control-id is added only for a requirement id NIST defines.
|
|
16012
|
+
*/
|
|
16013
|
+
function riskProps(override) {
|
|
16014
|
+
const props = [];
|
|
16015
|
+
pushVocabularyProp(props, "hdf-requirement-id", override.requirementId ?? "");
|
|
16016
|
+
if (nistExists(override.requirementId ?? "")) pushVocabularyProp(props, "impacted-control-id", nistTagToControlId(override.requirementId));
|
|
16017
|
+
pushVocabularyProp(props, "override-type", String(override.type));
|
|
16018
|
+
if (override.status !== void 0) pushVocabularyProp(props, "override-status", String(override.status));
|
|
16019
|
+
if (override.impact && typeof override.impact.value === "number") pushVocabularyProp(props, "impact-override", floatNumber(override.impact.value).token);
|
|
16020
|
+
if (override.justification !== void 0) pushVocabularyProp(props, "justification", String(override.justification));
|
|
16021
|
+
pushOptionalString(props, "baseline-ref", "baselineRef", override.baselineRef);
|
|
16022
|
+
pushOptionalString(props, "component-ref", "componentRef", override.componentRef);
|
|
16023
|
+
pushOptionalString(props, "inherited-from", "inheritedFrom", override.inheritedFrom);
|
|
16024
|
+
(override.affectedPackages ?? []).forEach((pkg, i) => {
|
|
16025
|
+
props.push(...affectedPackageProps(pkg, `package-${i + 1}`));
|
|
16026
|
+
});
|
|
16027
|
+
return props;
|
|
16028
|
+
}
|
|
16029
|
+
/** Carries one affected package, every prop in its group. */
|
|
16030
|
+
function affectedPackageProps(pkg, group) {
|
|
16031
|
+
const props = [];
|
|
16032
|
+
pushOptionalString(props, "affected-package-name", "name", pkg.name);
|
|
16033
|
+
pushOptionalString(props, "affected-package-version", "version", pkg.version);
|
|
16034
|
+
pushOptionalString(props, "affected-package-ecosystem", "ecosystem", pkg.ecosystem === void 0 ? void 0 : String(pkg.ecosystem));
|
|
16035
|
+
pushOptionalString(props, "affected-package-cpe", "cpe", pkg.cpe);
|
|
16036
|
+
pushOptionalString(props, "affected-package-purl", "purl", pkg.purl);
|
|
16037
|
+
pushOptionalString(props, "affected-package-fixed-in-version", "fixedInVersion", pkg.fixedInVersion);
|
|
16038
|
+
return props.map((p) => ({
|
|
16039
|
+
...p,
|
|
16040
|
+
group
|
|
16041
|
+
}));
|
|
16042
|
+
}
|
|
16043
|
+
/** Records when and by whom the override was applied, and its scheduled review at expiry. */
|
|
16044
|
+
function riskLogFor(override, applier, riskStatus) {
|
|
16045
|
+
const entries = [];
|
|
16046
|
+
const appliedAt = timestamp(override.appliedAt);
|
|
16047
|
+
if (appliedAt) entries.push({
|
|
16048
|
+
uuid: crypto.randomUUID(),
|
|
16049
|
+
title: OVERRIDE_APPLIED_TITLE,
|
|
16050
|
+
start: appliedAt,
|
|
16051
|
+
"logged-by": [{ "party-uuid": applier }]
|
|
16052
|
+
});
|
|
16053
|
+
const expiresAt = timestamp(override.expiresAt);
|
|
16054
|
+
if (expiresAt) entries.push({
|
|
16055
|
+
uuid: crypto.randomUUID(),
|
|
16056
|
+
title: "Scheduled review",
|
|
16057
|
+
description: "Amendment expiration date",
|
|
16058
|
+
start: expiresAt,
|
|
16059
|
+
"status-change": riskStatus
|
|
16060
|
+
});
|
|
16061
|
+
return entries.length > 0 ? { entries } : void 0;
|
|
16062
|
+
}
|
|
16063
|
+
/**
|
|
16064
|
+
* Renders each milestone as a planned remediation whose title and description are
|
|
16065
|
+
* the milestone's and whose task carries its title, schedule, status and
|
|
16066
|
+
* completion. An untitled milestone is titled "Milestone <n>" and marked
|
|
16067
|
+
* absent-field on both objects (§1.7.4). Mirrors milestoneRemediations in Go.
|
|
16068
|
+
*/
|
|
16069
|
+
function milestoneRemediations(milestones, b) {
|
|
16070
|
+
return milestones.map((ms, i) => {
|
|
16071
|
+
const marker = ms.title === void 0 ? [absentFieldProp("title")] : [];
|
|
16072
|
+
const title = ms.title ?? `Milestone ${i + 1}`;
|
|
16073
|
+
const rem = {
|
|
16074
|
+
uuid: crypto.randomUUID(),
|
|
16075
|
+
lifecycle: "planned",
|
|
16076
|
+
title,
|
|
16077
|
+
description: ms.description,
|
|
16078
|
+
...marker.length > 0 ? { props: marker } : {}
|
|
16079
|
+
};
|
|
16080
|
+
const eta = timestamp(ms.estimatedCompletion);
|
|
16081
|
+
if (eta) {
|
|
16082
|
+
const props = [...marker];
|
|
16083
|
+
pushVocabularyProp(props, "milestone-status", String(ms.status));
|
|
16084
|
+
const completedAt = timestamp(ms.completedAt);
|
|
16085
|
+
if (completedAt) pushVocabularyProp(props, "completed-at", completedAt);
|
|
16086
|
+
const task = {
|
|
16087
|
+
uuid: crypto.randomUUID(),
|
|
16088
|
+
type: "milestone",
|
|
16089
|
+
title,
|
|
16090
|
+
...props.length > 0 ? { props } : {},
|
|
16091
|
+
timing: { "within-date-range": {
|
|
16092
|
+
start: eta,
|
|
16093
|
+
end: eta
|
|
16094
|
+
} }
|
|
16095
|
+
};
|
|
16096
|
+
if (ms.completedBy) {
|
|
16097
|
+
b.completedBy = true;
|
|
16098
|
+
task["responsible-roles"] = [{
|
|
16099
|
+
"role-id": ROLE_COMPLETED_BY,
|
|
16100
|
+
"party-uuids": [b.parties.getOrAdd(ms.completedBy)]
|
|
16101
|
+
}];
|
|
16102
|
+
}
|
|
16103
|
+
rem.tasks = [task];
|
|
16104
|
+
}
|
|
16105
|
+
return rem;
|
|
16106
|
+
});
|
|
16107
|
+
}
|
|
16108
|
+
/**
|
|
14967
16109
|
* Returns the most recent override appliedAt for metadata.last-modified.
|
|
14968
|
-
* Sourcing it from the input keeps output deterministic
|
|
14969
|
-
*
|
|
14970
|
-
*
|
|
14971
|
-
* one).
|
|
16110
|
+
* Sourcing it from the input keeps output deterministic. Falls back to the wall
|
|
16111
|
+
* clock only when no override carries a date (appliedAt is schema-required, so
|
|
16112
|
+
* real documents always supply one).
|
|
14972
16113
|
*/
|
|
14973
16114
|
function latestAppliedAt(overrides) {
|
|
14974
16115
|
let latest;
|
|
@@ -14979,160 +16120,182 @@ function latestAppliedAt(overrides) {
|
|
|
14979
16120
|
return formatTimestampSeconds(latest ?? /* @__PURE__ */ new Date());
|
|
14980
16121
|
}
|
|
14981
16122
|
/**
|
|
14982
|
-
*
|
|
14983
|
-
*
|
|
14984
|
-
* value carries no more than an absent one and is schema-invalid. Mirrors
|
|
14985
|
-
* appendStringProp in the Go peer.
|
|
16123
|
+
* Sources metadata.version from the amendments document. OSCAL requires a
|
|
16124
|
+
* version, so an absent or empty one is written as 1.0.0 and marked by a metadata prop.
|
|
14986
16125
|
*/
|
|
14987
|
-
function pushStringProp(props, name, value) {
|
|
14988
|
-
const trimmed = oscalString(value ?? "");
|
|
14989
|
-
if (trimmed !== "") props.push({
|
|
14990
|
-
name,
|
|
14991
|
-
value: trimmed
|
|
14992
|
-
});
|
|
14993
|
-
}
|
|
14994
|
-
/** Sources metadata.version from the amendments document, defaulting when omitted. */
|
|
14995
16126
|
function amendmentsVersion(a) {
|
|
14996
16127
|
return oscalString(a.version ?? "") || "1.0.0";
|
|
14997
16128
|
}
|
|
14998
16129
|
/**
|
|
14999
|
-
* Carries document
|
|
15000
|
-
*
|
|
16130
|
+
* Carries the document fields that have no first-class OSCAL home and marks the
|
|
16131
|
+
* display fallbacks OSCAL forced. Labels are key/value prop pairs grouped in
|
|
16132
|
+
* sorted key order.
|
|
15001
16133
|
*/
|
|
15002
16134
|
function metadataProps(a) {
|
|
15003
16135
|
const props = [];
|
|
15004
|
-
|
|
15005
|
-
|
|
15006
|
-
|
|
15007
|
-
|
|
15008
|
-
|
|
16136
|
+
pushVocabularyProp(props, "amendments-name", a.name ?? "");
|
|
16137
|
+
pushOptionalString(props, "amendment-id", "amendmentId", a.amendmentId);
|
|
16138
|
+
if (a.description === "") props.push(emptyFieldProp("description"));
|
|
16139
|
+
pushFallbackMarker(props, "systemRef", a.systemRef);
|
|
16140
|
+
pushFallbackMarker(props, "version", a.version);
|
|
16141
|
+
Object.keys(a.labels ?? {}).sort(byCodePoint).forEach((key, i) => {
|
|
16142
|
+
const group = `label-${i + 1}`;
|
|
16143
|
+
props.push(labelProp("label-key", "key", key, group), labelProp("label-value", "value", a.labels[key], group));
|
|
16144
|
+
});
|
|
15009
16145
|
return props;
|
|
15010
16146
|
}
|
|
15011
16147
|
/**
|
|
15012
|
-
*
|
|
15013
|
-
*
|
|
15014
|
-
* encoded and the source key kept in remarks when it had to change. Every label
|
|
15015
|
-
* key in this package's converter fixtures is token-shaped today, so this guards a shape real
|
|
15016
|
-
* data has not yet produced — but Kubernetes and OCI label keys are namespaced
|
|
15017
|
-
* with '/', which HDF permits and OSCAL rejects.
|
|
15018
|
-
*
|
|
15019
|
-
* Mirrored by labelProp in the Go converter.
|
|
16148
|
+
* Marks an OSCAL-required field written as a display fallback: absent-field when
|
|
16149
|
+
* the HDF field is absent, empty-field when it is empty.
|
|
15020
16150
|
*/
|
|
15021
|
-
function
|
|
15022
|
-
|
|
15023
|
-
|
|
15024
|
-
|
|
15025
|
-
|
|
15026
|
-
|
|
16151
|
+
function pushFallbackMarker(props, field, value) {
|
|
16152
|
+
if (value === void 0) props.push(absentFieldProp(field));
|
|
16153
|
+
else if (value === "") props.push(emptyFieldProp(field));
|
|
16154
|
+
}
|
|
16155
|
+
/** Renders one half of an amendments label; an empty key or value is carried by empty-field (§1.7.3). */
|
|
16156
|
+
function labelProp(name, field, value, group) {
|
|
16157
|
+
return {
|
|
16158
|
+
...vocabularyProp(name, value) ?? emptyFieldProp(field),
|
|
16159
|
+
class: "amendment-label",
|
|
16160
|
+
group
|
|
15027
16161
|
};
|
|
15028
|
-
if (key !== "" && name !== key) prop.remarks = key;
|
|
15029
|
-
return prop;
|
|
15030
16162
|
}
|
|
15031
|
-
/**
|
|
15032
|
-
|
|
15033
|
-
|
|
16163
|
+
/** An RFC 3986 URI with a scheme, whose characters are all ones a URI may contain. */
|
|
16164
|
+
const ABSOLUTE_URI = /^[A-Za-z][A-Za-z0-9+.-]*:[A-Za-z0-9\-._~:/?#[\]@!$&'()*+,;=%]*$/;
|
|
16165
|
+
/** Whether evidence data is carried as the observation's relevant-evidence href rather than in a back-matter resource. */
|
|
16166
|
+
function evidenceDataIsHref(ev) {
|
|
16167
|
+
switch (String(ev.type)) {
|
|
16168
|
+
case "url": return true;
|
|
16169
|
+
case "screenshot":
|
|
16170
|
+
case "file": return ABSOLUTE_URI.test(ev.data);
|
|
16171
|
+
default: return false;
|
|
16172
|
+
}
|
|
15034
16173
|
}
|
|
15035
16174
|
/** Renders a single HDF Evidence item as an OSCAL observation. */
|
|
15036
|
-
function evidenceObservation(ev,
|
|
15037
|
-
const captured = ev.capturedAt ? toDate(ev.capturedAt) : void 0;
|
|
15038
|
-
const collected = captured ? formatTimestampSeconds(captured) : defaultCollected;
|
|
15039
|
-
const desc = ev.description && ev.description !== "" ? ev.description : "supporting evidence";
|
|
15040
|
-
const re = { description: desc };
|
|
15041
|
-
if (ev.type === "url") re.href = ev.data;
|
|
15042
|
-
else if (ev.data) re.remarks = ev.data;
|
|
16175
|
+
function evidenceObservation(ev, appliedAt, b) {
|
|
15043
16176
|
const props = [];
|
|
15044
|
-
|
|
15045
|
-
|
|
16177
|
+
let display = "Supporting evidence";
|
|
16178
|
+
let description = display;
|
|
16179
|
+
if (ev.description === void 0) props.push(absentFieldProp("description"));
|
|
16180
|
+
else {
|
|
16181
|
+
description = ev.description;
|
|
16182
|
+
display = displayLine(ev.description, display);
|
|
16183
|
+
}
|
|
16184
|
+
pushOptionalString(props, "mime-type", "mimeType", ev.mimeType);
|
|
16185
|
+
pushOptionalString(props, "evidence-encoding", "encoding", ev.encoding);
|
|
16186
|
+
if (typeof ev.size === "number") pushVocabularyProp(props, "evidence-size", floatNumber(ev.size).token);
|
|
16187
|
+
let collected = timestamp(ev.capturedAt);
|
|
16188
|
+
if (collected === void 0) {
|
|
16189
|
+
collected = timestamp(appliedAt) ?? formatTimestamp(/* @__PURE__ */ new Date());
|
|
16190
|
+
props.push(absentFieldProp("capturedAt"));
|
|
16191
|
+
}
|
|
16192
|
+
const re = { description: display };
|
|
16193
|
+
let links;
|
|
16194
|
+
if (evidenceDataIsHref(ev)) re.href = ev.data;
|
|
16195
|
+
else {
|
|
16196
|
+
const res = {
|
|
16197
|
+
uuid: crypto.randomUUID(),
|
|
16198
|
+
base64: {
|
|
16199
|
+
...ev.mimeType ? { "media-type": normalizePropValue(ev.mimeType) } : {},
|
|
16200
|
+
value: ev.encoding === "base64" ? ev.data : encodeBase64Utf8(ev.data)
|
|
16201
|
+
}
|
|
16202
|
+
};
|
|
16203
|
+
b.resources.push(res);
|
|
16204
|
+
links = [{
|
|
16205
|
+
href: `#${res.uuid}`,
|
|
16206
|
+
rel: "evidence"
|
|
16207
|
+
}];
|
|
16208
|
+
}
|
|
15046
16209
|
return {
|
|
15047
|
-
uuid,
|
|
15048
|
-
description
|
|
16210
|
+
uuid: crypto.randomUUID(),
|
|
16211
|
+
description,
|
|
16212
|
+
...props.length > 0 ? { props } : {},
|
|
16213
|
+
...links ? { links } : {},
|
|
15049
16214
|
methods: ["EXAMINE"],
|
|
15050
16215
|
types: [String(ev.type)],
|
|
16216
|
+
...ev.capturedBy ? { origins: [{ actors: [{
|
|
16217
|
+
type: "party",
|
|
16218
|
+
"actor-uuid": b.parties.getOrAdd(ev.capturedBy)
|
|
16219
|
+
}] }] } : {},
|
|
15051
16220
|
collected,
|
|
15052
|
-
...props.length > 0 ? { props } : {},
|
|
15053
16221
|
"relevant-evidence": [re]
|
|
15054
16222
|
};
|
|
15055
16223
|
}
|
|
15056
16224
|
/**
|
|
15057
|
-
*
|
|
15058
|
-
*
|
|
16225
|
+
* Carries an HDF Cvss record as one risk characterization: a facet per present
|
|
16226
|
+
* field in the CVSS system for its version, attributed to the override's applier.
|
|
15059
16227
|
*/
|
|
15060
|
-
function
|
|
16228
|
+
function cvssCharacterizations(c, applier) {
|
|
16229
|
+
if (!c) return [];
|
|
15061
16230
|
const system = `http://www.first.org/cvss/v${String(c.version)}`;
|
|
15062
|
-
const
|
|
15063
|
-
|
|
15064
|
-
|
|
15065
|
-
value
|
|
15066
|
-
|
|
15067
|
-
|
|
15068
|
-
|
|
16231
|
+
const props = [];
|
|
16232
|
+
const facets = [];
|
|
16233
|
+
const add = (name, field, value) => {
|
|
16234
|
+
if (value === void 0 || value === null) return;
|
|
16235
|
+
if (value === "") {
|
|
16236
|
+
props.push(emptyFieldProp(field));
|
|
16237
|
+
return;
|
|
16238
|
+
}
|
|
16239
|
+
const facet = {
|
|
15069
16240
|
name,
|
|
15070
16241
|
system,
|
|
15071
|
-
value
|
|
15072
|
-
}
|
|
16242
|
+
value: normalizePropValue(value)
|
|
16243
|
+
};
|
|
16244
|
+
if (facet.value !== value) facet.remarks = value;
|
|
16245
|
+
facets.push(facet);
|
|
15073
16246
|
};
|
|
15074
|
-
|
|
15075
|
-
|
|
15076
|
-
|
|
15077
|
-
add("
|
|
15078
|
-
|
|
15079
|
-
add("
|
|
15080
|
-
add("
|
|
15081
|
-
|
|
15082
|
-
add("
|
|
15083
|
-
|
|
15084
|
-
add("
|
|
15085
|
-
|
|
15086
|
-
|
|
15087
|
-
|
|
15088
|
-
|
|
15089
|
-
|
|
15090
|
-
|
|
15091
|
-
|
|
15092
|
-
|
|
15093
|
-
|
|
15094
|
-
|
|
15095
|
-
|
|
15096
|
-
|
|
15097
|
-
if (!o.externalReferences) continue;
|
|
15098
|
-
for (const ref of o.externalReferences) {
|
|
15099
|
-
const res = {
|
|
15100
|
-
uuid: crypto.randomUUID(),
|
|
15101
|
-
title: ref.sourceName
|
|
15102
|
-
};
|
|
15103
|
-
if (ref.description && ref.description !== "") res.description = ref.description;
|
|
15104
|
-
if (ref.href && ref.href !== "") res.rlinks = [{ href: ref.href }];
|
|
15105
|
-
const props = [];
|
|
15106
|
-
if (ref.sourceName) props.push({
|
|
15107
|
-
name: "source-name",
|
|
15108
|
-
value: ref.sourceName
|
|
15109
|
-
});
|
|
15110
|
-
if (ref.externalId) props.push({
|
|
15111
|
-
name: "external-id",
|
|
15112
|
-
value: ref.externalId
|
|
15113
|
-
});
|
|
15114
|
-
if (props.length > 0) res.props = props;
|
|
15115
|
-
resources.push(res);
|
|
15116
|
-
}
|
|
15117
|
-
}
|
|
15118
|
-
return resources;
|
|
16247
|
+
const score = (name, field, value) => {
|
|
16248
|
+
if (typeof value === "number") add(name, field, floatNumber(value).token);
|
|
16249
|
+
};
|
|
16250
|
+
add("cvss_version", "version", String(c.version));
|
|
16251
|
+
score("base_score", "baseScore", c.baseScore);
|
|
16252
|
+
add("base_severity", "baseSeverity", c.baseSeverity === void 0 ? void 0 : String(c.baseSeverity));
|
|
16253
|
+
add("base_vector", "baseVector", c.baseVector);
|
|
16254
|
+
score("threat_score", "threatScore", c.threatScore);
|
|
16255
|
+
add("threat_vector", "threatVector", c.threatVector);
|
|
16256
|
+
score("environmental_score", "environmentalScore", c.environmentalScore);
|
|
16257
|
+
add("environmental_vector", "environmentalVector", c.environmentalVector);
|
|
16258
|
+
score("computed_score", "computedScore", c.computedScore);
|
|
16259
|
+
add("computed_severity", "computedSeverity", c.computedSeverity === void 0 ? void 0 : String(c.computedSeverity));
|
|
16260
|
+
add("supplemental_vector", "supplementalVector", c.supplementalVector);
|
|
16261
|
+
add("source", "source", c.source);
|
|
16262
|
+
return [{
|
|
16263
|
+
...props.length > 0 ? { props } : {},
|
|
16264
|
+
origin: { actors: [{
|
|
16265
|
+
type: "party",
|
|
16266
|
+
"actor-uuid": applier
|
|
16267
|
+
}] },
|
|
16268
|
+
facets
|
|
16269
|
+
}];
|
|
15119
16270
|
}
|
|
15120
|
-
/**
|
|
15121
|
-
|
|
15122
|
-
* timing cannot express.
|
|
15123
|
-
*/
|
|
15124
|
-
function milestoneCompletionProps(ms) {
|
|
16271
|
+
/** Carries one external reference as a back-matter resource. */
|
|
16272
|
+
function referenceResource(ref, b) {
|
|
15125
16273
|
const props = [];
|
|
15126
|
-
|
|
15127
|
-
|
|
15128
|
-
|
|
15129
|
-
|
|
15130
|
-
|
|
15131
|
-
|
|
15132
|
-
|
|
15133
|
-
|
|
15134
|
-
|
|
15135
|
-
|
|
16274
|
+
pushVocabularyProp(props, "source-name", ref.sourceName ?? "");
|
|
16275
|
+
pushOptionalString(props, "external-id", "externalId", ref.externalId);
|
|
16276
|
+
if (ref.href === "") props.push(emptyFieldProp("href"));
|
|
16277
|
+
if (ref.description === "") props.push(emptyFieldProp("description"));
|
|
16278
|
+
pushOptionalString(props, "reference-rel", "rel", ref.rel);
|
|
16279
|
+
pushOptionalString(props, "reference-media-type", "mediaType", ref.mediaType);
|
|
16280
|
+
if (ref.checksum) {
|
|
16281
|
+
pushOptionalString(props, "checksum-algorithm", "checksum.algorithm", String(ref.checksum.algorithm));
|
|
16282
|
+
pushOptionalString(props, "checksum-value", "checksum.value", ref.checksum.value);
|
|
16283
|
+
}
|
|
16284
|
+
if (ref.addedBy) pushVocabularyProp(props, "added-by", b.parties.getOrAdd(ref.addedBy));
|
|
16285
|
+
const addedAt = timestamp(ref.addedAt);
|
|
16286
|
+
if (addedAt) pushVocabularyProp(props, "added-at", addedAt);
|
|
16287
|
+
pushOptionalString(props, "reference-kind", "kind", ref.kind);
|
|
16288
|
+
return {
|
|
16289
|
+
uuid: crypto.randomUUID(),
|
|
16290
|
+
title: ref.sourceName,
|
|
16291
|
+
...ref.description ? { description: ref.description } : {},
|
|
16292
|
+
...props.length > 0 ? { props } : {},
|
|
16293
|
+
...ref.href ? { rlinks: [{ href: ref.href }] } : {},
|
|
16294
|
+
...ref.document ? { base64: {
|
|
16295
|
+
"media-type": "application/json",
|
|
16296
|
+
value: encodeBase64Utf8(stringifyLine(canonicalize$1(ref.document)))
|
|
16297
|
+
} } : {}
|
|
16298
|
+
};
|
|
15136
16299
|
}
|
|
15137
16300
|
//#endregion
|
|
15138
16301
|
//#region converters/ionchannel-to-hdf/typescript/converter.ts
|
|
@@ -15231,7 +16394,8 @@ function analysisTimestamp(a) {
|
|
|
15231
16394
|
}
|
|
15232
16395
|
function buildScanRequirement(scan, analysis) {
|
|
15233
16396
|
const title = scan.description || `${titleCaseFirst(scan.name)} scan`;
|
|
15234
|
-
const
|
|
16397
|
+
const desc = scan.summary || `${titleCaseFirst(scan.name)} scan summary`;
|
|
16398
|
+
const descriptions = [createDescription("default", desc)];
|
|
15235
16399
|
const results = [{
|
|
15236
16400
|
status: ResultStatus.NotReviewed,
|
|
15237
16401
|
codeDesc: `${scan.name} scan summary`,
|
|
@@ -15296,11 +16460,13 @@ async function convertIonchannelToHdf(input, converterVersion = "1.0.0") {
|
|
|
15296
16460
|
outdated_version: dep.outdated_version,
|
|
15297
16461
|
dependencies: dep.dependencies
|
|
15298
16462
|
}, null, 2);
|
|
15299
|
-
const
|
|
16463
|
+
const descriptions = [createDescription("default", `Dependency ${dep.org}/${dep.name}`)];
|
|
16464
|
+
const results = [{
|
|
15300
16465
|
status: ResultStatus.NotReviewed,
|
|
15301
16466
|
codeDesc: "Dependency inventory item",
|
|
15302
16467
|
startTime: /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
|
|
15303
|
-
}]
|
|
16468
|
+
}];
|
|
16469
|
+
const req = createRequirement(depId, title, descriptions, 0, results, { tags });
|
|
15304
16470
|
req.code = code;
|
|
15305
16471
|
req.controlType = deriveControlTypeFromTags(DEFAULT_COMPONENT_MANAGEMENT_NIST_TAGS);
|
|
15306
16472
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
@@ -16128,7 +17294,7 @@ async function convertSpdxVexToHdf(input, converterVersion = "1.0.0") {
|
|
|
16128
17294
|
if (o) overrides.push(o);
|
|
16129
17295
|
}
|
|
16130
17296
|
if (overrides.length === 0) throw new Error("spdx-vex-to-hdf: SPDX document contains no actionable VEX statements (only affected/under_investigation); no amendment to write");
|
|
16131
|
-
const appliedBy = documentIdentity(graph, idx);
|
|
17297
|
+
const appliedBy = documentIdentity$1(graph, idx);
|
|
16132
17298
|
const name = appliedBy.identifier && appliedBy.identifier !== DEFAULT_APPLIED_BY ? `SPDX VEX statements from ${appliedBy.identifier}` : "SPDX VEX statements";
|
|
16133
17299
|
await chainOverrides(overrides);
|
|
16134
17300
|
return {
|
|
@@ -16238,7 +17404,7 @@ function identityFor(creationInfoRef, idx) {
|
|
|
16238
17404
|
identifier: DEFAULT_APPLIED_BY
|
|
16239
17405
|
};
|
|
16240
17406
|
}
|
|
16241
|
-
function documentIdentity(graph, idx) {
|
|
17407
|
+
function documentIdentity$1(graph, idx) {
|
|
16242
17408
|
for (const el of graph) if (el.type === "SpdxDocument") {
|
|
16243
17409
|
const id = identityFor(el.creationInfo, idx);
|
|
16244
17410
|
if (id.identifier !== DEFAULT_APPLIED_BY) return id;
|
|
@@ -16339,19 +17505,58 @@ function isCvssType(type) {
|
|
|
16339
17505
|
const GO_ZERO_TIME$1 = /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
|
|
16340
17506
|
const CVE_ID_PATTERN$2 = /^CVE-\d{4}-\d{4,}$/;
|
|
16341
17507
|
const PRODUCTS_LINE$2 = /^Products:\s*(.+)$/m;
|
|
16342
|
-
const DEFAULT_PRODUCT_ID
|
|
16343
|
-
|
|
16344
|
-
|
|
17508
|
+
const DEFAULT_PRODUCT_ID = "HDFPID-0001";
|
|
17509
|
+
const DEFAULT_PRODUCT_NAME = "No product identity was recorded in the source amendment";
|
|
17510
|
+
/**
|
|
17511
|
+
* Map an HDF Cvss_Severity band to the FIRST.org severityType enum: the same
|
|
17512
|
+
* five bands, which FIRST spells uppercase. Pinned in both languages to
|
|
17513
|
+
* fixtures/cvss-score-cases.json and to the vendored schema's enum.
|
|
17514
|
+
*/
|
|
17515
|
+
function csafSeverity(s) {
|
|
17516
|
+
return s.toUpperCase();
|
|
17517
|
+
}
|
|
17518
|
+
/**
|
|
17519
|
+
* Map an HDF Cvss block to a CSAF score entry, or undefined when the block
|
|
17520
|
+
* cannot be exported.
|
|
17521
|
+
*
|
|
17522
|
+
* A score is emitted only when every field the FIRST.org schema for its
|
|
17523
|
+
* version requires is present: version, vectorString and baseScore, plus
|
|
17524
|
+
* baseSeverity for 3.x and 4.0 (2.0 defines no severity, so none is emitted
|
|
17525
|
+
* for it). HDF requires only version, so legal HDF can fall short, and the
|
|
17526
|
+
* choice is to drop such a block rather than emit it partially: a partial
|
|
17527
|
+
* score fails the CSAF schema, and conforming consumers reject a document that
|
|
17528
|
+
* fails it, so it would cost every statement in the export, not just itself.
|
|
17529
|
+
* Nothing is synthesized to close the gap — a vector cannot be recovered from
|
|
17530
|
+
* a score, and a band the source never stated would be asserted in its name —
|
|
17531
|
+
* and the drop is logged so the loss is visible. A block with no base metrics
|
|
17532
|
+
* at all is consumer enrichment by design (threat or environmental deltas on a
|
|
17533
|
+
* riskAdjustment); CSAF scores carry base metrics, so nothing exportable was
|
|
17534
|
+
* lost and it is skipped silently.
|
|
17535
|
+
*/
|
|
17536
|
+
function buildCsafScore(cvss, cve, products) {
|
|
17537
|
+
const { version, baseVector, baseScore, baseSeverity } = cvss;
|
|
17538
|
+
const hasVector = Boolean(baseVector);
|
|
17539
|
+
const hasScore = typeof baseScore === "number";
|
|
17540
|
+
const hasSeverity = typeof baseSeverity === "string";
|
|
17541
|
+
if (!hasVector && !hasScore && !hasSeverity) return void 0;
|
|
17542
|
+
const v2 = version.startsWith("2");
|
|
17543
|
+
if (!baseVector || typeof baseScore !== "number" || !v2 && !hasSeverity) {
|
|
17544
|
+
emitConverterWarning(`hdf-to-csaf-vex: ${cve}: CVSS ${version} score not exported, CSAF requires ${[
|
|
17545
|
+
...hasVector ? [] : ["vectorString"],
|
|
17546
|
+
...hasScore ? [] : ["baseScore"],
|
|
17547
|
+
...v2 || hasSeverity ? [] : ["baseSeverity"]
|
|
17548
|
+
].join(", ")}`);
|
|
17549
|
+
return;
|
|
17550
|
+
}
|
|
16345
17551
|
const inner = {
|
|
16346
|
-
version
|
|
16347
|
-
|
|
16348
|
-
|
|
16349
|
-
...cvss.baseSeverity && { baseSeverity: cvss.baseSeverity }
|
|
17552
|
+
version,
|
|
17553
|
+
vectorString: baseVector,
|
|
17554
|
+
baseScore
|
|
16350
17555
|
};
|
|
16351
|
-
if (
|
|
17556
|
+
if (!v2 && hasSeverity) inner.baseSeverity = csafSeverity(baseSeverity);
|
|
16352
17557
|
return {
|
|
16353
|
-
|
|
16354
|
-
|
|
17558
|
+
products,
|
|
17559
|
+
[version.startsWith("4") ? "cvss_v4" : v2 ? "cvss_v2" : "cvss_v3"]: inner
|
|
16355
17560
|
};
|
|
16356
17561
|
}
|
|
16357
17562
|
function convertHdfToCsafVex(input, converterVersion) {
|
|
@@ -16374,8 +17579,8 @@ function convertHdfToCsafVex(input, converterVersion) {
|
|
|
16374
17579
|
const doc = buildDocument(amendments, converterVersion);
|
|
16375
17580
|
doc.vulnerabilities = vulnerabilities;
|
|
16376
17581
|
doc.product_tree.full_product_names = names.length > 0 ? names : [{
|
|
16377
|
-
name:
|
|
16378
|
-
product_id: DEFAULT_PRODUCT_ID
|
|
17582
|
+
name: DEFAULT_PRODUCT_NAME,
|
|
17583
|
+
product_id: DEFAULT_PRODUCT_ID
|
|
16379
17584
|
}];
|
|
16380
17585
|
return JSON.stringify(doc, null, 2);
|
|
16381
17586
|
}
|
|
@@ -16428,8 +17633,8 @@ function productEntriesFor(o) {
|
|
|
16428
17633
|
}));
|
|
16429
17634
|
}
|
|
16430
17635
|
return [{
|
|
16431
|
-
name:
|
|
16432
|
-
product_id: DEFAULT_PRODUCT_ID
|
|
17636
|
+
name: DEFAULT_PRODUCT_NAME,
|
|
17637
|
+
product_id: DEFAULT_PRODUCT_ID
|
|
16433
17638
|
}];
|
|
16434
17639
|
}
|
|
16435
17640
|
/** product_tree entries for the synthesized fixed-version products. */
|
|
@@ -16468,7 +17673,7 @@ function productIDsFor$1(o) {
|
|
|
16468
17673
|
const parts = m[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
16469
17674
|
if (parts.length > 0) return parts;
|
|
16470
17675
|
}
|
|
16471
|
-
return [DEFAULT_PRODUCT_ID
|
|
17676
|
+
return [DEFAULT_PRODUCT_ID];
|
|
16472
17677
|
}
|
|
16473
17678
|
function stripProductsLine$1(reason) {
|
|
16474
17679
|
return reason.replace(PRODUCTS_LINE$2, "").replace(/\n+$/, "");
|
|
@@ -16524,7 +17729,7 @@ function buildVulnerability(group) {
|
|
|
16524
17729
|
for (const o of group.overrides) {
|
|
16525
17730
|
const pids = productIDsFor$1(o);
|
|
16526
17731
|
if (o.cvss) {
|
|
16527
|
-
const score = buildCsafScore(o.cvss, pids);
|
|
17732
|
+
const score = buildCsafScore(o.cvss, group.cve, pids);
|
|
16528
17733
|
if (score) {
|
|
16529
17734
|
v.scores = (v.scores ?? []).concat(score);
|
|
16530
17735
|
emitted = true;
|
|
@@ -16852,7 +18057,6 @@ const CVE_ID_PATTERN = /^CVE-\d{4}-\d{4,}$/;
|
|
|
16852
18057
|
const PRODUCTS_LINE = /^Products:\s*(.+)$/m;
|
|
16853
18058
|
const RAW_JUST_LINE = /^VEX justification:\s*(.+)$/m;
|
|
16854
18059
|
const RESPONSE_LINE = /^Response:.*$/gm;
|
|
16855
|
-
const DEFAULT_PRODUCT_ID = "HDFPID-0001";
|
|
16856
18060
|
/**
|
|
16857
18061
|
* Build CycloneDX affects[].versions[] for a package with a fixedInVersion:
|
|
16858
18062
|
* the current version is `affected`, and `>= fixedInVersion` is `unaffected`,
|
|
@@ -16943,7 +18147,7 @@ async function convertHdfToCyclonedxVex(input, converterVersion) {
|
|
|
16943
18147
|
serialNumber: await buildSerialNumber(input, amendments),
|
|
16944
18148
|
version: 1,
|
|
16945
18149
|
metadata: buildMetadata(amendments, earliest ?? /* @__PURE__ */ new Date(), converterVersion, overrideIdentities),
|
|
16946
|
-
components,
|
|
18150
|
+
...components.length > 0 && { components },
|
|
16947
18151
|
vulnerabilities
|
|
16948
18152
|
};
|
|
16949
18153
|
return JSON.stringify(bom, null, 2);
|
|
@@ -16974,7 +18178,7 @@ function overrideToVulnerability(o, componentRegistry) {
|
|
|
16974
18178
|
references.push({
|
|
16975
18179
|
id: o.requirementId,
|
|
16976
18180
|
source: {
|
|
16977
|
-
name: e.description
|
|
18181
|
+
...e.description && { name: e.description },
|
|
16978
18182
|
url: e.data
|
|
16979
18183
|
}
|
|
16980
18184
|
});
|
|
@@ -17000,13 +18204,13 @@ function overrideToVulnerability(o, componentRegistry) {
|
|
|
17000
18204
|
...unranged && { recommendation: `Upgrade to ${unranged.fixedInVersion}` },
|
|
17001
18205
|
...advisories.length > 0 && { advisories },
|
|
17002
18206
|
analysis,
|
|
17003
|
-
affects: pids.map((pid) => {
|
|
18207
|
+
...pids.length > 0 && { affects: pids.map((pid) => {
|
|
17004
18208
|
const versions = buildCdxAffectsVersions(pkgById.get(pid));
|
|
17005
18209
|
return versions ? {
|
|
17006
18210
|
ref: pid,
|
|
17007
18211
|
versions
|
|
17008
18212
|
} : { ref: pid };
|
|
17009
|
-
})
|
|
18213
|
+
}) }
|
|
17010
18214
|
};
|
|
17011
18215
|
}
|
|
17012
18216
|
function canonicalToCycloneDXState(canonical) {
|
|
@@ -17040,7 +18244,7 @@ function productIDsFor(o) {
|
|
|
17040
18244
|
const parts = m[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
17041
18245
|
if (parts.length > 0) return parts;
|
|
17042
18246
|
}
|
|
17043
|
-
return [
|
|
18247
|
+
return [];
|
|
17044
18248
|
}
|
|
17045
18249
|
function stripReasonAnnotations(reason) {
|
|
17046
18250
|
return reason.replace(PRODUCTS_LINE, "").replace(RAW_JUST_LINE, "").replace(RESPONSE_LINE, "").trim();
|
|
@@ -17240,11 +18444,13 @@ async function convertOscalComponentToHdf(input) {
|
|
|
17240
18444
|
if (!compDef.components || compDef.components.length === 0) throw new Error("oscal-component-definition: document contains no components");
|
|
17241
18445
|
const integrity = await inputIntegrity(input);
|
|
17242
18446
|
const meta = extractMetadata(compDef.metadata);
|
|
17243
|
-
const comp = compDef.components[0];
|
|
17244
18447
|
const requirements = [];
|
|
17245
|
-
for (const
|
|
18448
|
+
for (const comp of compDef.components) for (const ci of comp["control-implementations"] ?? []) {
|
|
18449
|
+
const limited = limitArrayWithWarning(ci["implemented-requirements"] ?? [], "implemented requirement");
|
|
18450
|
+
for (const ir of limited) requirements.push(implementedRequirementToBaselineRequirement(ir, comp));
|
|
18451
|
+
}
|
|
17246
18452
|
const baseline = {
|
|
17247
|
-
name:
|
|
18453
|
+
name: definitionBaselineName(compDef),
|
|
17248
18454
|
title: meta.title,
|
|
17249
18455
|
version: meta.version,
|
|
17250
18456
|
status: "loaded",
|
|
@@ -17258,7 +18464,16 @@ async function convertOscalComponentToHdf(input) {
|
|
|
17258
18464
|
return JSON.stringify(baseline, null, 2);
|
|
17259
18465
|
}
|
|
17260
18466
|
/** Converts a single ImplementedRequirement to a BaselineRequirement. */
|
|
17261
|
-
|
|
18467
|
+
/**
|
|
18468
|
+
* Qualifies the control with the component's uuid. OSCAL requires both a uuid
|
|
18469
|
+
* and a title on every component but only guarantees the uuid is unique, so a
|
|
18470
|
+
* title would still collide when one product appears twice — the duplicate-ID
|
|
18471
|
+
* defect this qualification prevents. The title rides in a tag instead.
|
|
18472
|
+
*/
|
|
18473
|
+
function requirementId(comp, nistTag) {
|
|
18474
|
+
return comp?.uuid ? `${comp.uuid}/${nistTag}` : nistTag;
|
|
18475
|
+
}
|
|
18476
|
+
function implementedRequirementToBaselineRequirement(ir, comp) {
|
|
17262
18477
|
const nistTag = controlIdToNistTag(ir["control-id"]);
|
|
17263
18478
|
const descriptions = [];
|
|
17264
18479
|
descriptions.push({
|
|
@@ -17275,23 +18490,37 @@ function implementedRequirementToBaselineRequirement(ir) {
|
|
|
17275
18490
|
data: stmt.remarks
|
|
17276
18491
|
});
|
|
17277
18492
|
}
|
|
18493
|
+
const tags = { nist: [nistTag] };
|
|
18494
|
+
if (comp?.title) tags.component = comp.title;
|
|
18495
|
+
if (comp?.uuid) tags.componentUuid = comp.uuid;
|
|
17278
18496
|
return {
|
|
17279
|
-
id: nistTag,
|
|
18497
|
+
id: requirementId(comp, nistTag),
|
|
17280
18498
|
title: nistTag,
|
|
17281
18499
|
impact: .5,
|
|
17282
18500
|
descriptions,
|
|
17283
|
-
tags
|
|
18501
|
+
tags
|
|
17284
18502
|
};
|
|
17285
18503
|
}
|
|
17286
|
-
/**
|
|
17287
|
-
|
|
17288
|
-
|
|
18504
|
+
/**
|
|
18505
|
+
* Derives the baseline name: a single-component definition is named for its
|
|
18506
|
+
* component, a multi-component one for the definition itself.
|
|
18507
|
+
*/
|
|
18508
|
+
function definitionBaselineName(compDef) {
|
|
18509
|
+
let name = compDef.metadata.title;
|
|
18510
|
+
if (compDef.components?.length === 1 && compDef.components[0].title) name = compDef.components[0].title;
|
|
18511
|
+
return toKebabCase(name, "oscal-component-definition");
|
|
17289
18512
|
}
|
|
17290
18513
|
//#endregion
|
|
17291
18514
|
//#region converters/oscal-to-hdf/typescript/converter-poam.ts
|
|
17292
18515
|
/**
|
|
17293
18516
|
* OSCAL Plan of Action and Milestones (POA&M) to HDF Amendments converter.
|
|
17294
18517
|
*
|
|
18518
|
+
* A POA&M hdf-to-oscal-poam produced returns every amendments field it carries
|
|
18519
|
+
* (ADR-0014 §4.6), apart from the integrity fields and generator that contract
|
|
18520
|
+
* excludes. Foreign POA&Ms, and HDF exports from before the ADR (§4.3), keep the
|
|
18521
|
+
* mapping they had before it: type "poam", with their own extension props not
|
|
18522
|
+
* carried (§3.6).
|
|
18523
|
+
*
|
|
17295
18524
|
* Mirrors the Go implementation in converters/oscal-to-hdf/go/converter_poam.go.
|
|
17296
18525
|
*/
|
|
17297
18526
|
/**
|
|
@@ -17310,30 +18539,113 @@ async function convertOscalPoamToHdf(input) {
|
|
|
17310
18539
|
const meta = extractMetadata(poam.metadata);
|
|
17311
18540
|
const riskMap = buildRiskMap$1(poam.risks ?? []);
|
|
17312
18541
|
const overrides = [];
|
|
17313
|
-
|
|
17314
|
-
const
|
|
17315
|
-
|
|
18542
|
+
let hdfProduced = false;
|
|
18543
|
+
for (const item of poam["poam-items"]) {
|
|
18544
|
+
const override = poamItemToOverride(item, riskMap, poam);
|
|
18545
|
+
if (!override) continue;
|
|
18546
|
+
if (hdfProducedRisk(item, riskMap)) hdfProduced = true;
|
|
18547
|
+
overrides.push(override);
|
|
18548
|
+
}
|
|
18549
|
+
if (overrides.length === 0) throw new Error(`oscal-poam-to-hdf: no poam-item names a requirement; every one of the ${poam["poam-items"].length} items was skipped`);
|
|
17316
18550
|
await chainOverrides(overrides);
|
|
17317
|
-
|
|
18551
|
+
const preparedBy = firstResponsibleParty(poam.metadata, "prepared-by");
|
|
18552
|
+
const amendments = {
|
|
17318
18553
|
name: toKebabCase(poam.metadata.title, "oscal-poam"),
|
|
17319
18554
|
overrides,
|
|
17320
18555
|
integrity,
|
|
17321
|
-
systemRef,
|
|
18556
|
+
systemRef: poam["import-ssp"]?.href || void 0,
|
|
17322
18557
|
version: meta.version,
|
|
17323
|
-
appliedBy
|
|
18558
|
+
appliedBy: preparedBy === "" ? void 0 : {
|
|
18559
|
+
type: IdentityType.Simple,
|
|
18560
|
+
identifier: preparedBy
|
|
18561
|
+
},
|
|
17324
18562
|
generator: {
|
|
17325
18563
|
name: "oscal-poam-to-hdf",
|
|
17326
18564
|
version: "1.0.0"
|
|
17327
18565
|
}
|
|
17328
|
-
}
|
|
18566
|
+
};
|
|
18567
|
+
if (hdfProduced) readHdfDocumentFields(poam, amendments);
|
|
18568
|
+
return serializeHdf(amendments);
|
|
18569
|
+
}
|
|
18570
|
+
/**
|
|
18571
|
+
* Reads the amendments document fields from their ADR-0014 §4.6 homes, which a
|
|
18572
|
+
* POA&M carries only when one of its risks is HDF-produced (§4.3).
|
|
18573
|
+
*/
|
|
18574
|
+
function readHdfDocumentFields(poam, amendments) {
|
|
18575
|
+
const props = poam.metadata.props;
|
|
18576
|
+
amendments.name = findGroupedVocabularyProp(props, "amendments-name", "")?.value ?? "";
|
|
18577
|
+
if (poam.metadata.remarks) amendments.description = poam.metadata.remarks;
|
|
18578
|
+
else if (hasFieldMarker(props, "empty-field", "description", "")) amendments.description = "";
|
|
18579
|
+
amendments.appliedBy = documentIdentity(poam, "prepared-by");
|
|
18580
|
+
const approvedBy = partyIdentity(poam, firstResponsibleParty(poam.metadata, "approved-by"));
|
|
18581
|
+
if (approvedBy) amendments.approvedBy = approvedBy;
|
|
18582
|
+
const amendmentId = vocabularyString(props, "amendment-id", "amendmentId", "");
|
|
18583
|
+
if (amendmentId !== void 0) amendments.amendmentId = amendmentId;
|
|
18584
|
+
const labels = amendmentLabels(props);
|
|
18585
|
+
if (labels) amendments.labels = labels;
|
|
18586
|
+
amendments.systemRef = markedField(props, "systemRef", amendments.systemRef);
|
|
18587
|
+
amendments.version = markedField(props, "version", amendments.version);
|
|
18588
|
+
}
|
|
18589
|
+
/**
|
|
18590
|
+
* Applies the metadata markers for an OSCAL-required field the exporter wrote a
|
|
18591
|
+
* display fallback for: absent-field leaves it absent and empty-field returns it empty.
|
|
18592
|
+
*/
|
|
18593
|
+
function markedField(props, field, value) {
|
|
18594
|
+
if (hasFieldMarker(props, "absent-field", field, "")) return void 0;
|
|
18595
|
+
if (hasFieldMarker(props, "empty-field", field, "")) return "";
|
|
18596
|
+
return value;
|
|
18597
|
+
}
|
|
18598
|
+
/** Reads the label-key and label-value pairs, one per label-<n> group. */
|
|
18599
|
+
function amendmentLabels(props) {
|
|
18600
|
+
const groups = /* @__PURE__ */ new Set();
|
|
18601
|
+
for (const m of [...findVocabularyProps(props, "label-key"), ...findVocabularyProps(props, "empty-field")]) {
|
|
18602
|
+
const p = props[m.index];
|
|
18603
|
+
if (p.class === "amendment-label" && p.group) groups.add(p.group);
|
|
18604
|
+
}
|
|
18605
|
+
if (groups.size === 0) return void 0;
|
|
18606
|
+
const labels = {};
|
|
18607
|
+
for (const group of groups) {
|
|
18608
|
+
const key = vocabularyString(props, "label-key", "key", group);
|
|
18609
|
+
const value = vocabularyString(props, "label-value", "value", group);
|
|
18610
|
+
if (key !== void 0 && value !== void 0) labels[key] = value;
|
|
18611
|
+
}
|
|
18612
|
+
return labels;
|
|
17329
18613
|
}
|
|
17330
18614
|
function buildRiskMap$1(risks) {
|
|
17331
18615
|
const m = /* @__PURE__ */ new Map();
|
|
17332
18616
|
for (const risk of risks) m.set(risk.uuid, risk);
|
|
17333
18617
|
return m;
|
|
17334
18618
|
}
|
|
18619
|
+
/**
|
|
18620
|
+
* The item's first related risk that carries override-type in the HDF namespace,
|
|
18621
|
+
* which makes it HDF-produced (ADR-0014 §4.3), or undefined.
|
|
18622
|
+
*/
|
|
18623
|
+
function hdfProducedRisk(item, riskMap) {
|
|
18624
|
+
return overrideTypeRisk(item, riskMap, false);
|
|
18625
|
+
}
|
|
18626
|
+
/** The item's first related risk whose override-type prop is (legacy) or is not (!legacy) matched through the pre-ADR fallback. */
|
|
18627
|
+
function overrideTypeRisk(item, riskMap, legacy) {
|
|
18628
|
+
for (const rr of item["related-risks"] ?? []) {
|
|
18629
|
+
const risk = rr["risk-uuid"] ? riskMap.get(rr["risk-uuid"]) : void 0;
|
|
18630
|
+
if (risk && findVocabularyProp(risk.props, "override-type")?.legacy === legacy) return risk;
|
|
18631
|
+
}
|
|
18632
|
+
}
|
|
18633
|
+
/** Converts a poam-item to an override, or undefined for a pre-ADR item with no requirement id, which is skipped. */
|
|
17335
18634
|
function poamItemToOverride(item, riskMap, poam) {
|
|
17336
|
-
const
|
|
18635
|
+
const risk = hdfProducedRisk(item, riskMap);
|
|
18636
|
+
if (risk) {
|
|
18637
|
+
if (hdfRequirementId(risk) === void 0) {
|
|
18638
|
+
emitConverterWarning(`Skipping poam-item "${item.uuid}" titled "${item.title}": its HDF-produced risk has no hdf-requirement-id`);
|
|
18639
|
+
return;
|
|
18640
|
+
}
|
|
18641
|
+
return hdfOverride(item, risk, riskMap, poam);
|
|
18642
|
+
}
|
|
18643
|
+
const requirementId = foreignRequirementId(item, riskMap);
|
|
18644
|
+
if (requirementId === void 0) {
|
|
18645
|
+
emitConverterWarning(`Skipping poam-item "${item.uuid}" titled "${item.title}": its pre-ADR risk has no impacted-control-id`);
|
|
18646
|
+
return;
|
|
18647
|
+
}
|
|
18648
|
+
const milestones = extractMilestones(item, riskMap);
|
|
17337
18649
|
return {
|
|
17338
18650
|
type: OverrideType.Poam,
|
|
17339
18651
|
requirementId,
|
|
@@ -17342,9 +18654,306 @@ function poamItemToOverride(item, riskMap, poam) {
|
|
|
17342
18654
|
appliedBy: poamItemAppliedBy(poam),
|
|
17343
18655
|
appliedAt: poamItemAppliedAt(poam, requirementId),
|
|
17344
18656
|
expiresAt: poamItemExpiresAt(item, riskMap, requirementId),
|
|
17345
|
-
milestones:
|
|
18657
|
+
...milestones.length > 0 ? { milestones } : {}
|
|
18658
|
+
};
|
|
18659
|
+
}
|
|
18660
|
+
/**
|
|
18661
|
+
* The requirement id an HDF-produced risk carries. An absent or empty prop names
|
|
18662
|
+
* no requirement, and StandaloneOverride.requirementId is required and non-empty,
|
|
18663
|
+
* so the item is skipped rather than imported unusable.
|
|
18664
|
+
*/
|
|
18665
|
+
function hdfRequirementId(risk) {
|
|
18666
|
+
return findVocabularyProp(risk.props, "hdf-requirement-id")?.value || void 0;
|
|
18667
|
+
}
|
|
18668
|
+
/**
|
|
18669
|
+
* Reads every override field from its ADR-0014 §4.6 home on an HDF-produced risk.
|
|
18670
|
+
* An absent prop is an absent field: nothing is derived from titles or uuids.
|
|
18671
|
+
*/
|
|
18672
|
+
function hdfOverride(item, risk, riskMap, poam) {
|
|
18673
|
+
const props = risk.props;
|
|
18674
|
+
const requirementId = hdfRequirementId(risk) ?? "";
|
|
18675
|
+
const entry = overrideAppliedEntry(risk);
|
|
18676
|
+
const appliedAt = entry && parseTimestamp(String(entry.start)) || poamItemAppliedAt(poam, requirementId);
|
|
18677
|
+
const override = {
|
|
18678
|
+
type: findVocabularyProp(props, "override-type").value,
|
|
18679
|
+
requirementId,
|
|
18680
|
+
reason: item.description,
|
|
18681
|
+
appliedBy: poamItemAppliedBy(poam),
|
|
18682
|
+
appliedAt,
|
|
18683
|
+
expiresAt: poamItemExpiresAt(item, riskMap, requirementId)
|
|
18684
|
+
};
|
|
18685
|
+
const status = findVocabularyProp(props, "override-status");
|
|
18686
|
+
if (status) override.status = status.value;
|
|
18687
|
+
const impact = findVocabularyProp(props, "impact-override");
|
|
18688
|
+
if (impact && impact.value.trim() !== "" && !isNaN(Number(impact.value))) override.impact = { value: Number(impact.value) };
|
|
18689
|
+
const justification = findVocabularyProp(props, "justification");
|
|
18690
|
+
if (justification) override.justification = justification.value;
|
|
18691
|
+
const baselineRef = vocabularyString(props, "baseline-ref", "baselineRef", "");
|
|
18692
|
+
if (baselineRef !== void 0) override.baselineRef = baselineRef;
|
|
18693
|
+
const componentRef = vocabularyString(props, "component-ref", "componentRef", "");
|
|
18694
|
+
if (componentRef !== void 0) override.componentRef = componentRef;
|
|
18695
|
+
const inheritedFrom = vocabularyString(props, "inherited-from", "inheritedFrom", "");
|
|
18696
|
+
if (inheritedFrom !== void 0) override.inheritedFrom = inheritedFrom;
|
|
18697
|
+
const packages = affectedPackages(props);
|
|
18698
|
+
if (packages.length > 0) override.affectedPackages = packages;
|
|
18699
|
+
const loggedBy = entry?.["logged-by"]?.[0]?.["party-uuid"];
|
|
18700
|
+
const applier = loggedBy === void 0 ? void 0 : partyIdentity(poam, loggedBy);
|
|
18701
|
+
if (applier) override.appliedBy = applier;
|
|
18702
|
+
const milestones = hdfMilestones(risk, poam);
|
|
18703
|
+
if (milestones.length > 0) override.milestones = milestones;
|
|
18704
|
+
const cvss = riskCvss(risk);
|
|
18705
|
+
if (cvss) override.cvss = cvss;
|
|
18706
|
+
const evidence = itemEvidence(item, poam);
|
|
18707
|
+
if (evidence.length > 0) override.evidence = evidence;
|
|
18708
|
+
const refs = riskReferences(risk, poam);
|
|
18709
|
+
if (refs.length > 0) override.externalReferences = refs;
|
|
18710
|
+
return override;
|
|
18711
|
+
}
|
|
18712
|
+
/** The risk-log entry recording when the override was applied. */
|
|
18713
|
+
function overrideAppliedEntry(risk) {
|
|
18714
|
+
return risk["risk-log"]?.entries.find((e) => e.title === "Override applied");
|
|
18715
|
+
}
|
|
18716
|
+
/** Reads the package-<n> prop groups in position order. */
|
|
18717
|
+
function affectedPackages(props) {
|
|
18718
|
+
const positions = /* @__PURE__ */ new Set();
|
|
18719
|
+
for (const p of props ?? []) {
|
|
18720
|
+
const m = /^package-(\d+)$/.exec(p.group ?? "");
|
|
18721
|
+
if (m && consumedVocabularyProp(p) && Number(m[1]) > 0) positions.add(Number(m[1]));
|
|
18722
|
+
}
|
|
18723
|
+
return [...positions].sort((a, b) => a - b).map((position) => {
|
|
18724
|
+
const group = `package-${position}`;
|
|
18725
|
+
const pkg = {};
|
|
18726
|
+
const name = vocabularyString(props, "affected-package-name", "name", group);
|
|
18727
|
+
if (name !== void 0) pkg.name = name;
|
|
18728
|
+
const version = vocabularyString(props, "affected-package-version", "version", group);
|
|
18729
|
+
if (version !== void 0) pkg.version = version;
|
|
18730
|
+
const ecosystem = vocabularyString(props, "affected-package-ecosystem", "ecosystem", group);
|
|
18731
|
+
if (ecosystem !== void 0) pkg.ecosystem = ecosystem;
|
|
18732
|
+
const cpe = vocabularyString(props, "affected-package-cpe", "cpe", group);
|
|
18733
|
+
if (cpe !== void 0) pkg.cpe = cpe;
|
|
18734
|
+
const purl = vocabularyString(props, "affected-package-purl", "purl", group);
|
|
18735
|
+
if (purl !== void 0) pkg.purl = purl;
|
|
18736
|
+
const fixedInVersion = vocabularyString(props, "affected-package-fixed-in-version", "fixedInVersion", group);
|
|
18737
|
+
if (fixedInVersion !== void 0) pkg.fixedInVersion = fixedInVersion;
|
|
18738
|
+
return pkg;
|
|
18739
|
+
});
|
|
18740
|
+
}
|
|
18741
|
+
/** The first party uuid metadata assigns to roleId, or ''. */
|
|
18742
|
+
function firstResponsibleParty(meta, roleId) {
|
|
18743
|
+
for (const rp of meta["responsible-parties"] ?? []) if (rp["role-id"] === roleId && rp["party-uuids"].length > 0) return rp["party-uuids"][0];
|
|
18744
|
+
return "";
|
|
18745
|
+
}
|
|
18746
|
+
/** Reads the HDF identity a metadata party carries, or undefined for a party that carries none. */
|
|
18747
|
+
function partyIdentity(poam, partyUuid) {
|
|
18748
|
+
const party = (poam.metadata.parties ?? []).find((p) => p.uuid === partyUuid);
|
|
18749
|
+
if (!party) return void 0;
|
|
18750
|
+
const identityType = findVocabularyProp(party.props, "identity-type");
|
|
18751
|
+
if (!identityType) return void 0;
|
|
18752
|
+
const id = {
|
|
18753
|
+
identifier: findVocabularyProp(party.props, "identity-identifier")?.value ?? "",
|
|
18754
|
+
type: identityType.value
|
|
18755
|
+
};
|
|
18756
|
+
if (party.remarks) id.description = party.remarks;
|
|
18757
|
+
else if (hasFieldMarker(party.props, "empty-field", "description", "")) id.description = "";
|
|
18758
|
+
return id;
|
|
18759
|
+
}
|
|
18760
|
+
/** The identity metadata assigns to roleId: the HDF identity its party carries, or else the party uuid as a simple identifier. */
|
|
18761
|
+
function documentIdentity(poam, roleId) {
|
|
18762
|
+
const partyUuid = firstResponsibleParty(poam.metadata, roleId);
|
|
18763
|
+
if (partyUuid === "") return void 0;
|
|
18764
|
+
return partyIdentity(poam, partyUuid) ?? {
|
|
18765
|
+
type: IdentityType.Simple,
|
|
18766
|
+
identifier: partyUuid
|
|
17346
18767
|
};
|
|
17347
18768
|
}
|
|
18769
|
+
/**
|
|
18770
|
+
* Mirrors the schema's Milestone.title pattern: an OSCAL task title is
|
|
18771
|
+
* markup-line, which a foreign producer may wrap or pad.
|
|
18772
|
+
*/
|
|
18773
|
+
const MILESTONE_TITLE_PATTERN = /^[^ \t\n\r]([^\n\r]*[^ \t\n\r])?$/;
|
|
18774
|
+
/** Reads each planned remediation task of an HDF-produced risk as a milestone whose description is the remediation description. */
|
|
18775
|
+
function hdfMilestones(risk, poam) {
|
|
18776
|
+
const milestones = [];
|
|
18777
|
+
for (const rem of risk.remediations ?? []) {
|
|
18778
|
+
if (rem.lifecycle !== "planned") continue;
|
|
18779
|
+
for (const task of rem.tasks ?? []) {
|
|
18780
|
+
const end = task.timing?.["within-date-range"]?.end;
|
|
18781
|
+
const estimatedCompletion = end && parseTimestamp(String(end));
|
|
18782
|
+
if (!estimatedCompletion) continue;
|
|
18783
|
+
const status = findVocabularyProp(task.props, "milestone-status");
|
|
18784
|
+
const ms = {
|
|
18785
|
+
description: rem.description,
|
|
18786
|
+
estimatedCompletion,
|
|
18787
|
+
status: status?.value ?? MilestoneStatus.Pending
|
|
18788
|
+
};
|
|
18789
|
+
if (!hasFieldMarker(task.props, "absent-field", "title", "")) {
|
|
18790
|
+
const title = task.title ?? "";
|
|
18791
|
+
if (MILESTONE_TITLE_PATTERN.test(title)) ms.title = title;
|
|
18792
|
+
else emitConverterWarning(`Dropping the title of task "${task.uuid}": ${JSON.stringify(title)} is not the single line Milestone.title is`);
|
|
18793
|
+
}
|
|
18794
|
+
const completedAt = findVocabularyProp(task.props, "completed-at");
|
|
18795
|
+
const completedAtDate = completedAt && parseTimestamp(completedAt.value);
|
|
18796
|
+
if (completedAtDate) ms.completedAt = completedAtDate;
|
|
18797
|
+
for (const rr of task["responsible-roles"] ?? []) {
|
|
18798
|
+
const partyUuid = rr["party-uuids"]?.[0];
|
|
18799
|
+
const completer = rr["role-id"] === "completed-by" && partyUuid !== void 0 ? partyIdentity(poam, partyUuid) : void 0;
|
|
18800
|
+
if (completer) ms.completedBy = completer;
|
|
18801
|
+
}
|
|
18802
|
+
milestones.push(ms);
|
|
18803
|
+
}
|
|
18804
|
+
}
|
|
18805
|
+
return milestones;
|
|
18806
|
+
}
|
|
18807
|
+
/** Begins the facet system of every CVSS version. */
|
|
18808
|
+
const CVSS_SYSTEM_PREFIX = "http://www.first.org/cvss/v";
|
|
18809
|
+
/** Reads the characterization whose facets all use a CVSS system. */
|
|
18810
|
+
function riskCvss(risk) {
|
|
18811
|
+
for (const ch of risk.characterizations ?? []) {
|
|
18812
|
+
if (ch.facets.length === 0 || !ch.facets.every((f) => f.system.startsWith(CVSS_SYSTEM_PREFIX))) continue;
|
|
18813
|
+
const values = new Map(ch.facets.map((f) => [f.name, f.remarks || f.value]));
|
|
18814
|
+
const str = (name, field) => values.get(name) ?? (hasFieldMarker(ch.props, "empty-field", field, "") ? "" : void 0);
|
|
18815
|
+
const score = (name) => {
|
|
18816
|
+
const value = values.get(name);
|
|
18817
|
+
return value !== void 0 && value.trim() !== "" && !isNaN(Number(value)) ? Number(value) : void 0;
|
|
18818
|
+
};
|
|
18819
|
+
const severity = (name) => values.get(name);
|
|
18820
|
+
return {
|
|
18821
|
+
version: values.get("cvss_version"),
|
|
18822
|
+
baseScore: score("base_score"),
|
|
18823
|
+
baseSeverity: severity("base_severity"),
|
|
18824
|
+
baseVector: str("base_vector", "baseVector"),
|
|
18825
|
+
threatScore: score("threat_score"),
|
|
18826
|
+
threatVector: str("threat_vector", "threatVector"),
|
|
18827
|
+
environmentalScore: score("environmental_score"),
|
|
18828
|
+
environmentalVector: str("environmental_vector", "environmentalVector"),
|
|
18829
|
+
computedScore: score("computed_score"),
|
|
18830
|
+
computedSeverity: severity("computed_severity"),
|
|
18831
|
+
supplementalVector: str("supplemental_vector", "supplementalVector"),
|
|
18832
|
+
source: str("source", "source")
|
|
18833
|
+
};
|
|
18834
|
+
}
|
|
18835
|
+
}
|
|
18836
|
+
/**
|
|
18837
|
+
* Reads the evidence observations an HDF-produced item lists. An observation with
|
|
18838
|
+
* no payload is not an HDF evidence item (ADR-0014 §4.6 gives Evidence.data no
|
|
18839
|
+
* other home) and Evidence.data is required and non-empty, so a foreign
|
|
18840
|
+
* observation listed alongside the exported ones is skipped.
|
|
18841
|
+
*/
|
|
18842
|
+
function itemEvidence(item, poam) {
|
|
18843
|
+
const evidence = [];
|
|
18844
|
+
for (const ro of item["related-observations"] ?? []) {
|
|
18845
|
+
const obs = (poam.observations ?? []).find((o) => o.uuid === ro["observation-uuid"]);
|
|
18846
|
+
if (!obs) continue;
|
|
18847
|
+
const ev = observationEvidence(obs, poam);
|
|
18848
|
+
if (ev.data === "") {
|
|
18849
|
+
emitConverterWarning(`Skipping evidence observation "${obs.uuid}": no relevant-evidence href and no evidence resource`);
|
|
18850
|
+
continue;
|
|
18851
|
+
}
|
|
18852
|
+
evidence.push(ev);
|
|
18853
|
+
}
|
|
18854
|
+
return evidence;
|
|
18855
|
+
}
|
|
18856
|
+
/** Decodes base64 to the UTF-8 text it encodes, or undefined when it is not base64. */
|
|
18857
|
+
function decodeBase64Utf8(value) {
|
|
18858
|
+
try {
|
|
18859
|
+
const binary = atob(value);
|
|
18860
|
+
return new TextDecoder().decode(Uint8Array.from(binary, (c) => c.charCodeAt(0)));
|
|
18861
|
+
} catch {
|
|
18862
|
+
return;
|
|
18863
|
+
}
|
|
18864
|
+
}
|
|
18865
|
+
/** Reads one evidence item from its observation (ADR-0014 §4.6 Evidence table). */
|
|
18866
|
+
function observationEvidence(obs, poam) {
|
|
18867
|
+
const props = obs.props;
|
|
18868
|
+
const ev = {
|
|
18869
|
+
type: obs.types?.[0] ?? "",
|
|
18870
|
+
data: ""
|
|
18871
|
+
};
|
|
18872
|
+
if (!hasFieldMarker(props, "absent-field", "description", "")) ev.description = obs.description;
|
|
18873
|
+
const mimeType = vocabularyString(props, "mime-type", "mimeType", "");
|
|
18874
|
+
if (mimeType !== void 0) ev.mimeType = mimeType;
|
|
18875
|
+
const encoding = vocabularyString(props, "evidence-encoding", "encoding", "");
|
|
18876
|
+
if (encoding !== void 0) ev.encoding = encoding;
|
|
18877
|
+
const size = findVocabularyProp(props, "evidence-size");
|
|
18878
|
+
if (size && size.value.trim() !== "" && !isNaN(Number(size.value))) ev.size = Number(size.value);
|
|
18879
|
+
if (!hasFieldMarker(props, "absent-field", "capturedAt", "")) {
|
|
18880
|
+
const capturedAt = parseTimestamp(String(obs.collected));
|
|
18881
|
+
if (capturedAt) ev.capturedAt = capturedAt;
|
|
18882
|
+
}
|
|
18883
|
+
for (const origin of obs.origins ?? []) for (const actor of origin.actors) {
|
|
18884
|
+
const capturer = actor.type === "party" && ev.capturedBy === void 0 ? partyIdentity(poam, actor["actor-uuid"]) : void 0;
|
|
18885
|
+
if (capturer) ev.capturedBy = capturer;
|
|
18886
|
+
}
|
|
18887
|
+
const href = obs["relevant-evidence"]?.[0]?.href;
|
|
18888
|
+
if (href) {
|
|
18889
|
+
ev.data = href;
|
|
18890
|
+
return ev;
|
|
18891
|
+
}
|
|
18892
|
+
for (const link of obs.links ?? []) {
|
|
18893
|
+
const res = backMatterResource(poam, link);
|
|
18894
|
+
if (link.rel !== "evidence" || !res?.base64) continue;
|
|
18895
|
+
ev.data = encoding === "base64" ? res.base64.value : decodeBase64Utf8(res.base64.value) ?? res.base64.value;
|
|
18896
|
+
break;
|
|
18897
|
+
}
|
|
18898
|
+
return ev;
|
|
18899
|
+
}
|
|
18900
|
+
/** Resolves a link to the back-matter resource its "#<uuid>" href names. */
|
|
18901
|
+
function backMatterResource(poam, link) {
|
|
18902
|
+
if (!link.href.startsWith("#")) return void 0;
|
|
18903
|
+
return (poam["back-matter"]?.resources ?? []).find((r) => r.uuid === link.href.slice(1));
|
|
18904
|
+
}
|
|
18905
|
+
/** Reads the external references an HDF-produced risk links to (ADR-0014 §4.6 External reference table). */
|
|
18906
|
+
function riskReferences(risk, poam) {
|
|
18907
|
+
const refs = [];
|
|
18908
|
+
for (const link of risk.links ?? []) {
|
|
18909
|
+
const res = backMatterResource(poam, link);
|
|
18910
|
+
if (link.rel !== "reference" || !res) continue;
|
|
18911
|
+
const props = res.props;
|
|
18912
|
+
const ref = { sourceName: findVocabularyProp(props, "source-name")?.value ?? "" };
|
|
18913
|
+
const externalId = vocabularyString(props, "external-id", "externalId", "");
|
|
18914
|
+
if (externalId !== void 0) ref.externalId = externalId;
|
|
18915
|
+
if (res.rlinks && res.rlinks.length > 0) ref.href = res.rlinks[0].href;
|
|
18916
|
+
else if (hasFieldMarker(props, "empty-field", "href", "")) ref.href = "";
|
|
18917
|
+
if (res.description) ref.description = res.description;
|
|
18918
|
+
else if (hasFieldMarker(props, "empty-field", "description", "")) ref.description = "";
|
|
18919
|
+
const rel = vocabularyString(props, "reference-rel", "rel", "");
|
|
18920
|
+
if (rel !== void 0) ref.rel = rel;
|
|
18921
|
+
const mediaType = vocabularyString(props, "reference-media-type", "mediaType", "");
|
|
18922
|
+
if (mediaType !== void 0) ref.mediaType = mediaType;
|
|
18923
|
+
const algorithm = vocabularyString(props, "checksum-algorithm", "checksum.algorithm", "");
|
|
18924
|
+
if (algorithm !== void 0) ref.checksum = {
|
|
18925
|
+
algorithm,
|
|
18926
|
+
value: vocabularyString(props, "checksum-value", "checksum.value", "") ?? ""
|
|
18927
|
+
};
|
|
18928
|
+
const addedBy = findVocabularyProp(props, "added-by");
|
|
18929
|
+
const adder = addedBy ? partyIdentity(poam, addedBy.value) : void 0;
|
|
18930
|
+
if (adder) ref.addedBy = adder;
|
|
18931
|
+
const addedAt = findVocabularyProp(props, "added-at");
|
|
18932
|
+
const addedAtDate = addedAt && parseTimestamp(addedAt.value);
|
|
18933
|
+
if (addedAtDate) ref.addedAt = addedAtDate;
|
|
18934
|
+
const kind = vocabularyString(props, "reference-kind", "kind", "");
|
|
18935
|
+
if (kind !== void 0) ref.kind = kind;
|
|
18936
|
+
const documentJson = res.base64 ? decodeBase64Utf8(res.base64.value) : void 0;
|
|
18937
|
+
if (documentJson !== void 0) try {
|
|
18938
|
+
const document = JSON.parse(documentJson);
|
|
18939
|
+
if (document !== null && typeof document === "object" && !Array.isArray(document)) ref.document = document;
|
|
18940
|
+
} catch {}
|
|
18941
|
+
refs.push(ref);
|
|
18942
|
+
}
|
|
18943
|
+
return refs;
|
|
18944
|
+
}
|
|
18945
|
+
/**
|
|
18946
|
+
* The requirement id of an item that is not HDF-produced. A pre-ADR HDF export's id
|
|
18947
|
+
* comes only from its legacy impacted-control-id, never from a title (ADR-0014 §4.3),
|
|
18948
|
+
* so it is undefined for one that has none, which the conversion skips. Mirrors
|
|
18949
|
+
* foreignRequirementID in Go.
|
|
18950
|
+
*/
|
|
18951
|
+
function foreignRequirementId(item, riskMap) {
|
|
18952
|
+
const preADR = overrideTypeRisk(item, riskMap, true);
|
|
18953
|
+
if (!preADR) return extractRequirementIdFromPOAMItem(item, riskMap);
|
|
18954
|
+
const control = findVocabularyProp(preADR.props, "impacted-control-id");
|
|
18955
|
+
return control ? controlIdToNistTag(control.value) : void 0;
|
|
18956
|
+
}
|
|
17348
18957
|
function extractRequirementIdFromPOAMItem(item, riskMap) {
|
|
17349
18958
|
for (const rr of item["related-risks"] ?? []) {
|
|
17350
18959
|
const riskUuid = rr["risk-uuid"];
|
|
@@ -17381,17 +18990,16 @@ function poamItemStatus(item, riskMap) {
|
|
|
17381
18990
|
return ResultStatus.Failed;
|
|
17382
18991
|
}
|
|
17383
18992
|
function poamItemAppliedBy(poam) {
|
|
18993
|
+
const preparedBy = firstResponsibleParty(poam.metadata, "prepared-by");
|
|
18994
|
+
if (preparedBy !== "") return {
|
|
18995
|
+
type: IdentityType.Simple,
|
|
18996
|
+
identifier: preparedBy
|
|
18997
|
+
};
|
|
17384
18998
|
const rps = poam.metadata["responsible-parties"];
|
|
17385
|
-
if (rps) {
|
|
17386
|
-
|
|
17387
|
-
|
|
17388
|
-
|
|
17389
|
-
};
|
|
17390
|
-
if (rps.length > 0 && rps[0]["party-uuids"].length > 0) return {
|
|
17391
|
-
type: IdentityType.Simple,
|
|
17392
|
-
identifier: rps[0]["party-uuids"][0]
|
|
17393
|
-
};
|
|
17394
|
-
}
|
|
18999
|
+
if (rps && rps.length > 0 && rps[0]["party-uuids"].length > 0) return {
|
|
19000
|
+
type: IdentityType.Simple,
|
|
19001
|
+
identifier: rps[0]["party-uuids"][0]
|
|
19002
|
+
};
|
|
17395
19003
|
return {
|
|
17396
19004
|
type: IdentityType.System,
|
|
17397
19005
|
identifier: "oscal-poam-converter"
|
|
@@ -17436,15 +19044,6 @@ function extractMilestones(item, riskMap) {
|
|
|
17436
19044
|
}
|
|
17437
19045
|
return milestones;
|
|
17438
19046
|
}
|
|
17439
|
-
function extractAppliedBy(meta) {
|
|
17440
|
-
const rps = meta["responsible-parties"];
|
|
17441
|
-
if (rps) {
|
|
17442
|
-
for (const rp of rps) if (rp["role-id"] === "prepared-by" && rp["party-uuids"].length > 0) return {
|
|
17443
|
-
type: IdentityType.Simple,
|
|
17444
|
-
identifier: rp["party-uuids"][0]
|
|
17445
|
-
};
|
|
17446
|
-
}
|
|
17447
|
-
}
|
|
17448
19047
|
//#endregion
|
|
17449
19048
|
//#region converters/oscal-to-hdf/typescript/converter-sap.ts
|
|
17450
19049
|
/**
|
|
@@ -17598,11 +19197,18 @@ async function convertOscalSarToHdf(input) {
|
|
|
17598
19197
|
const scanTime = /* @__PURE__ */ new Date();
|
|
17599
19198
|
const baselines = [];
|
|
17600
19199
|
for (const result of sar.results) {
|
|
19200
|
+
const title = result.title || result.uuid;
|
|
17601
19201
|
if (!result.findings || result.findings.length === 0) {
|
|
17602
|
-
|
|
19202
|
+
emitConverterWarning(`Skipping assessment result "${title}": no findings (empty result set)`);
|
|
19203
|
+
continue;
|
|
19204
|
+
}
|
|
19205
|
+
const { groups, skipped } = groupFindingsByRequirement(result.findings);
|
|
19206
|
+
for (const f of skipped) emitConverterWarning(`Skipping finding "${f.uuid}" titled "${f.title}": empty target-id`);
|
|
19207
|
+
if (groups.size === 0) {
|
|
19208
|
+
emitConverterWarning(`Skipping assessment result "${title}": no finding has a target-id`);
|
|
17603
19209
|
continue;
|
|
17604
19210
|
}
|
|
17605
|
-
const baseline = await resultToEvaluatedBaseline(result, sar, input, scanTime);
|
|
19211
|
+
const baseline = await resultToEvaluatedBaseline(result, groups, sar, input, scanTime);
|
|
17606
19212
|
baselines.push(baseline);
|
|
17607
19213
|
}
|
|
17608
19214
|
const planRef = sar["import-ap"]?.href || void 0;
|
|
@@ -17625,27 +19231,51 @@ async function convertOscalSarToHdf(input) {
|
|
|
17625
19231
|
planRef
|
|
17626
19232
|
});
|
|
17627
19233
|
}
|
|
17628
|
-
|
|
19234
|
+
/**
|
|
19235
|
+
* Groups findings by requirement id in first-seen order (a Map keeps insertion
|
|
19236
|
+
* order), within the finding cap, and returns the findings skipped for an empty
|
|
19237
|
+
* target-id. Mirrors Go's groupFindingsByRequirement.
|
|
19238
|
+
*/
|
|
19239
|
+
function groupFindingsByRequirement(findings) {
|
|
19240
|
+
const groups = /* @__PURE__ */ new Map();
|
|
19241
|
+
const skipped = [];
|
|
19242
|
+
for (const f of limitArrayWithWarning(findings, "finding")) {
|
|
19243
|
+
const id = sarRequirementId(f);
|
|
19244
|
+
if (id === void 0) {
|
|
19245
|
+
skipped.push(f);
|
|
19246
|
+
continue;
|
|
19247
|
+
}
|
|
19248
|
+
const existing = groups.get(id);
|
|
19249
|
+
if (existing) existing.push(f);
|
|
19250
|
+
else groups.set(id, [f]);
|
|
19251
|
+
}
|
|
19252
|
+
return {
|
|
19253
|
+
groups,
|
|
19254
|
+
skipped
|
|
19255
|
+
};
|
|
19256
|
+
}
|
|
19257
|
+
/**
|
|
19258
|
+
* The HDF requirement id a finding belongs to (ADR-0014 §4.5): the HDF-namespaced
|
|
19259
|
+
* hdf-requirement-id when present; else the NIST control a roster-confirmed
|
|
19260
|
+
* target names; else the target-id verbatim. Undefined for a finding whose
|
|
19261
|
+
* target-id is empty. Mirrors Go's sarRequirementID.
|
|
19262
|
+
*/
|
|
19263
|
+
function sarRequirementId(f) {
|
|
19264
|
+
const targetId = f.target["target-id"];
|
|
19265
|
+
if (!targetId) return void 0;
|
|
19266
|
+
const prop = findVocabularyProp(f.props, "hdf-requirement-id");
|
|
19267
|
+
if (prop && prop.value !== "") return prop.value;
|
|
19268
|
+
const controlId = confirmedControlId(targetId);
|
|
19269
|
+
return controlId === void 0 ? targetId : controlIdToNistTag(controlId);
|
|
19270
|
+
}
|
|
19271
|
+
async function resultToEvaluatedBaseline(result, groups, sar, rawInput, scanTime) {
|
|
17629
19272
|
const checksum = await inputChecksum(rawInput);
|
|
17630
19273
|
const obsMap = buildObservationMap(result.observations ?? []);
|
|
17631
19274
|
const riskMap = buildRiskMap(result.risks ?? []);
|
|
17632
|
-
const controlOrder = [];
|
|
17633
|
-
const controlMap = /* @__PURE__ */ new Map();
|
|
17634
|
-
for (const f of result.findings ?? []) {
|
|
17635
|
-
const controlId = extractControlIdFromFinding(f);
|
|
17636
|
-
const existing = controlMap.get(controlId);
|
|
17637
|
-
if (existing) existing.push(f);
|
|
17638
|
-
else {
|
|
17639
|
-
controlOrder.push(controlId);
|
|
17640
|
-
controlMap.set(controlId, [f]);
|
|
17641
|
-
}
|
|
17642
|
-
}
|
|
17643
19275
|
const requirements = [];
|
|
17644
|
-
for (const
|
|
17645
|
-
|
|
17646
|
-
|
|
17647
|
-
}
|
|
17648
|
-
const baseline = createMinimalBaseline(sarBaselineName(result, sar), requirements, {
|
|
19276
|
+
for (const [id, findings] of groups) requirements.push(findingsToEvaluatedRequirement(id, findings, obsMap, riskMap, result, scanTime));
|
|
19277
|
+
const name = sarBaselineName(result, sar);
|
|
19278
|
+
const baseline = createMinimalBaseline(name, requirements, {
|
|
17649
19279
|
resultsChecksum: checksum,
|
|
17650
19280
|
integrity: await inputIntegrity(rawInput),
|
|
17651
19281
|
status: "loaded",
|
|
@@ -17654,17 +19284,17 @@ async function resultToEvaluatedBaseline(result, sar, rawInput, scanTime) {
|
|
|
17654
19284
|
if (result.description) baseline.description = result.description;
|
|
17655
19285
|
return baseline;
|
|
17656
19286
|
}
|
|
17657
|
-
function findingsToEvaluatedRequirement(
|
|
17658
|
-
const
|
|
17659
|
-
const title = findings[0].title || nistTag;
|
|
19287
|
+
function findingsToEvaluatedRequirement(id, findings, obsMap, riskMap, result, scanTime) {
|
|
19288
|
+
const title = findings[0].title || id;
|
|
17660
19289
|
const impact = sarFindingsImpact(findings, riskMap);
|
|
17661
19290
|
const descriptions = sarBuildDescriptions(findings, obsMap, riskMap);
|
|
17662
19291
|
const refs = sarBuildRefs(findings, obsMap);
|
|
17663
19292
|
const results = [];
|
|
17664
19293
|
for (const f of findings) results.push(findingToRequirementResult(f, obsMap, riskMap, result, scanTime));
|
|
17665
|
-
const nistTags =
|
|
17666
|
-
const
|
|
17667
|
-
|
|
19294
|
+
const nistTags = sarConfirmedNistTags(findings);
|
|
19295
|
+
const tags = buildNistCciTags(nistTags, nistToCci(nistTags));
|
|
19296
|
+
const req = createRequirement(id, title, descriptions, impact, results, {
|
|
19297
|
+
tags,
|
|
17668
19298
|
...refs ? { refs } : {}
|
|
17669
19299
|
});
|
|
17670
19300
|
const controlType = deriveControlTypeFromTags(nistTags);
|
|
@@ -17686,13 +19316,14 @@ function findingToRequirementResult(f, obsMap, riskMap, result, scanTime) {
|
|
|
17686
19316
|
startTime
|
|
17687
19317
|
});
|
|
17688
19318
|
}
|
|
17689
|
-
|
|
17690
|
-
|
|
17691
|
-
|
|
17692
|
-
|
|
17693
|
-
|
|
17694
|
-
|
|
17695
|
-
|
|
19319
|
+
/** The distinct NIST controls, in NIST notation, the findings' targets confirm, in finding order. */
|
|
19320
|
+
function sarConfirmedNistTags(findings) {
|
|
19321
|
+
const controlIds = [];
|
|
19322
|
+
for (const f of findings) {
|
|
19323
|
+
const controlId = confirmedControlId(f.target["target-id"]);
|
|
19324
|
+
if (controlId !== void 0) controlIds.push(controlId);
|
|
19325
|
+
}
|
|
19326
|
+
return controlIdsToNistTags(controlIds);
|
|
17696
19327
|
}
|
|
17697
19328
|
function mapFindingStatus(f) {
|
|
17698
19329
|
const status = oscalStatusToHdf(f.target.status.state);
|
|
@@ -17751,19 +19382,19 @@ function sarBuildDescriptions(findings, obsMap, riskMap) {
|
|
|
17751
19382
|
label: "default",
|
|
17752
19383
|
data: findingDescs.join("\n") || ""
|
|
17753
19384
|
});
|
|
17754
|
-
const
|
|
17755
|
-
|
|
17756
|
-
for (const f of findings) for (const ref of f["related-observations"] ?? []) {
|
|
17757
|
-
const obsUuid = ref["observation-uuid"];
|
|
17758
|
-
if (!obsUuid || seen.has(obsUuid)) continue;
|
|
17759
|
-
seen.add(obsUuid);
|
|
17760
|
-
const obs = obsMap.get(obsUuid);
|
|
17761
|
-
if (obs?.description) obsDescs.push(obs.description);
|
|
17762
|
-
}
|
|
17763
|
-
if (obsDescs.length > 0) descriptions.push({
|
|
19385
|
+
const rationales = findings.map((f) => f.target.description ?? "").filter((d) => d !== "");
|
|
19386
|
+
if (rationales.length > 0) descriptions.push({
|
|
17764
19387
|
label: "rationale",
|
|
17765
|
-
data:
|
|
19388
|
+
data: rationales.join("\n")
|
|
17766
19389
|
});
|
|
19390
|
+
const labelled = collectLabelledProse(findings, obsMap, riskMap);
|
|
19391
|
+
for (const label of ["check", "fix"]) {
|
|
19392
|
+
const texts = labelled.get(label);
|
|
19393
|
+
if (texts) descriptions.push({
|
|
19394
|
+
label,
|
|
19395
|
+
data: texts.join("\n")
|
|
19396
|
+
});
|
|
19397
|
+
}
|
|
17767
19398
|
const statement = collectRiskStatements(findings, riskMap);
|
|
17768
19399
|
if (statement) descriptions.push({
|
|
17769
19400
|
label: "statement",
|
|
@@ -17781,6 +19412,46 @@ function sarBuildDescriptions(findings, obsMap, riskMap) {
|
|
|
17781
19412
|
});
|
|
17782
19413
|
return descriptions;
|
|
17783
19414
|
}
|
|
19415
|
+
/** The HDF description label a relevant-evidence entry carries ("check" or "fix"), or '' for foreign evidence. */
|
|
19416
|
+
function evidenceDescriptionLabel(ev) {
|
|
19417
|
+
const label = descriptionLabel(ev.props);
|
|
19418
|
+
return label === "check" || label === "fix" ? label : "";
|
|
19419
|
+
}
|
|
19420
|
+
/** Whether a remediation is HDF's fix prose home. */
|
|
19421
|
+
function isLabelledFix(rem) {
|
|
19422
|
+
return descriptionLabel(rem.props) === "fix";
|
|
19423
|
+
}
|
|
19424
|
+
/**
|
|
19425
|
+
* Gathers the check and fix text HDF wrote into labelled relevant-evidence
|
|
19426
|
+
* entries (full text from remarks) and labelled remediations, in finding order,
|
|
19427
|
+
* reading each observation and risk once.
|
|
19428
|
+
*/
|
|
19429
|
+
function collectLabelledProse(findings, obsMap, riskMap) {
|
|
19430
|
+
const texts = /* @__PURE__ */ new Map();
|
|
19431
|
+
const add = (label, text) => {
|
|
19432
|
+
texts.set(label, [...texts.get(label) ?? [], text]);
|
|
19433
|
+
};
|
|
19434
|
+
const seenObs = /* @__PURE__ */ new Set();
|
|
19435
|
+
const seenRisk = /* @__PURE__ */ new Set();
|
|
19436
|
+
for (const f of findings) {
|
|
19437
|
+
for (const ref of f["related-observations"] ?? []) {
|
|
19438
|
+
const obs = obsMap.get(ref["observation-uuid"]);
|
|
19439
|
+
if (!obs || seenObs.has(ref["observation-uuid"])) continue;
|
|
19440
|
+
seenObs.add(ref["observation-uuid"]);
|
|
19441
|
+
for (const ev of obs["relevant-evidence"] ?? []) {
|
|
19442
|
+
const label = evidenceDescriptionLabel(ev);
|
|
19443
|
+
if (label !== "") add(label, ev.remarks || ev.description);
|
|
19444
|
+
}
|
|
19445
|
+
}
|
|
19446
|
+
for (const ref of f["related-risks"] ?? []) {
|
|
19447
|
+
const risk = riskMap.get(ref["risk-uuid"]);
|
|
19448
|
+
if (!risk || seenRisk.has(ref["risk-uuid"])) continue;
|
|
19449
|
+
seenRisk.add(ref["risk-uuid"]);
|
|
19450
|
+
for (const rem of risk.remediations ?? []) if (isLabelledFix(rem)) add("fix", rem.description);
|
|
19451
|
+
}
|
|
19452
|
+
}
|
|
19453
|
+
return texts;
|
|
19454
|
+
}
|
|
17784
19455
|
function collectRiskStatements(findings, riskMap) {
|
|
17785
19456
|
const statements = [];
|
|
17786
19457
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -17802,6 +19473,7 @@ function collectRemediations(findings, riskMap) {
|
|
|
17802
19473
|
seen.add(riskUuid);
|
|
17803
19474
|
const risk = riskMap.get(riskUuid);
|
|
17804
19475
|
for (const rem of risk?.remediations ?? []) {
|
|
19476
|
+
if (isLabelledFix(rem)) continue;
|
|
17805
19477
|
const text = remediationText(rem);
|
|
17806
19478
|
if (text) remediations.push(text);
|
|
17807
19479
|
}
|
|
@@ -17823,7 +19495,7 @@ function collectEvidenceDescriptions(findings, obsMap) {
|
|
|
17823
19495
|
seenObs.add(obsUuid);
|
|
17824
19496
|
const obs = obsMap.get(obsUuid);
|
|
17825
19497
|
for (const ev of obs?.["relevant-evidence"] ?? []) {
|
|
17826
|
-
if (!ev.description || seenText.has(ev.description)) continue;
|
|
19498
|
+
if (!ev.description || seenText.has(ev.description) || evidenceDescriptionLabel(ev) !== "") continue;
|
|
17827
19499
|
seenText.add(ev.description);
|
|
17828
19500
|
descs.push(ev.description);
|
|
17829
19501
|
}
|
|
@@ -18024,6 +19696,7 @@ function sspComponentToHDFComponent(sc, componentControls) {
|
|
|
18024
19696
|
name: sc.title,
|
|
18025
19697
|
type: mapOSCALComponentType(sc.type)
|
|
18026
19698
|
};
|
|
19699
|
+
if (sc.uuid) comp.componentId = sc.uuid;
|
|
18027
19700
|
if (sc.description) comp.description = sc.description;
|
|
18028
19701
|
const controls = componentControls.get(sc.uuid);
|
|
18029
19702
|
if (controls && controls.size > 0) {
|
|
@@ -18252,7 +19925,7 @@ function stixObjectCVEs(obj) {
|
|
|
18252
19925
|
}
|
|
18253
19926
|
//#endregion
|
|
18254
19927
|
//#region shared/typescript/enrichStix.ts
|
|
18255
|
-
const DEFAULT_REVIEW_HORIZON_MS =
|
|
19928
|
+
const DEFAULT_REVIEW_HORIZON_MS = 7776e6;
|
|
18256
19929
|
/** Format a Date as canonical trimmed-UTC RFC3339 (seconds precision, matching Go's time.RFC3339). */
|
|
18257
19930
|
function toRfc3339(d) {
|
|
18258
19931
|
return d.toISOString().replace(/\.\d{3}Z$/, "Z");
|