@mitre/hdf-converters 3.4.3 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/index.d.ts +89 -27
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1317 -385
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { n as detectConverter, t as registerAllFingerprints } from "./register-all-WRR-vAZf.js";
|
|
2
2
|
import { flattenOverlays } from "@mitre/hdf-parsers";
|
|
3
|
-
import { buildCsv, buildXml, cvssScoreToSeverity, formatTimestamp, formatTimestampSeconds, impactToSeverity, normalizeHdfTimestamps, parseCsv, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, severityToImpact, sha256, stripHtml as stripHTML, trimUtcFraction } from "@mitre/hdf-utilities";
|
|
4
|
-
import { Applicability, AuthorizationStatus, CVSSSeverity, CategorizationLevel, ControlType, Ecosystem, EvidenceType, HashAlgorithm, IdentityType, Justification, MilestoneStatus, OverrideType, PlanType, ResultStatus, TargetType, VerificationMethodEnum, Version, createDescription, createMinimalBaseline, createRequirement, createResult, severityToImpact as severityToImpact$1 } from "@mitre/hdf-schema";
|
|
3
|
+
import { buildCsv, buildXml, computeCvssScore, computeEffectiveStatus, cvssScoreToSeverity, formatTimestamp, formatTimestampSeconds, impactToSeverity, normalizeHdfTimestamps, parseCsv, parseJSON, parsePurl, parseTimestamp, parseXml, parseXmlWithArrays, roundImpact, severityToImpact, sha256, stripHtml as stripHTML, trimUtcFraction, worstStatus } from "@mitre/hdf-utilities";
|
|
4
|
+
import { Applicability, AuthorizationStatus, CVSSSeverity, CategorizationLevel, ControlType, Ecosystem, EvidenceType, HashAlgorithm, IdentityType, Justification, MilestoneStatus, OverrideType, PlanType, ResultStatus, Severity, TargetType, VerificationMethodEnum, Version, createDescription, createMinimalBaseline, createRequirement, createResult, severityToImpact as severityToImpact$1 } from "@mitre/hdf-schema";
|
|
5
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";
|
|
6
6
|
//#region shared/typescript/converterutil.ts
|
|
7
7
|
/**
|
|
@@ -150,6 +150,7 @@ const DEFAULT_MAX_INPUT_SIZE = 50 * 1024 * 1024;
|
|
|
150
150
|
* @throws Error if input exceeds maxSize
|
|
151
151
|
*/
|
|
152
152
|
function validateInputSize(input, converterName, maxSize = DEFAULT_MAX_INPUT_SIZE) {
|
|
153
|
+
if (maxSize <= 0) maxSize = DEFAULT_MAX_INPUT_SIZE;
|
|
153
154
|
if (input.length > maxSize) throw new Error(`${converterName}: input exceeds maximum allowed size of ${maxSize} characters`);
|
|
154
155
|
}
|
|
155
156
|
/**
|
|
@@ -487,35 +488,15 @@ function normalizeStatus$1(status) {
|
|
|
487
488
|
}[status] || "notReviewed";
|
|
488
489
|
}
|
|
489
490
|
/**
|
|
490
|
-
* Compute effectiveStatus from impact and v2 results
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
* error > failed > passed > notApplicable > notReviewed
|
|
494
|
-
*
|
|
495
|
-
* See docs/design/status-determination.md for full specification.
|
|
491
|
+
* Compute effectiveStatus from impact and v2 results via the canonical
|
|
492
|
+
* worst-wins ordering in @mitre/hdf-utilities (impact=0 → notApplicable;
|
|
493
|
+
* error > failed > passed > notApplicable > notReviewed).
|
|
496
494
|
*/
|
|
497
|
-
function computeEffectiveStatus(impact, results) {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
let hasNotApplicable = false;
|
|
503
|
-
for (const r of results) switch (r.status) {
|
|
504
|
-
case "error": return "error";
|
|
505
|
-
case "failed":
|
|
506
|
-
hasFailed = true;
|
|
507
|
-
break;
|
|
508
|
-
case "passed":
|
|
509
|
-
hasPassed = true;
|
|
510
|
-
break;
|
|
511
|
-
case "notApplicable":
|
|
512
|
-
hasNotApplicable = true;
|
|
513
|
-
break;
|
|
514
|
-
}
|
|
515
|
-
if (hasFailed) return "failed";
|
|
516
|
-
if (hasPassed) return "passed";
|
|
517
|
-
if (hasNotApplicable) return "notApplicable";
|
|
518
|
-
return "notReviewed";
|
|
495
|
+
function computeEffectiveStatus$1(impact, results) {
|
|
496
|
+
return computeEffectiveStatus({
|
|
497
|
+
impact,
|
|
498
|
+
resultStatuses: results.map((r) => r.status)
|
|
499
|
+
});
|
|
519
500
|
}
|
|
520
501
|
/**
|
|
521
502
|
* Convert v1.0 result to v2.0 result.
|
|
@@ -613,7 +594,7 @@ function convertControl(v1Control) {
|
|
|
613
594
|
if (v1Control.source_location !== void 0) v2Req.sourceLocation = v1Control.source_location;
|
|
614
595
|
if (v1Control.status !== void 0) v2Req.effectiveStatus = normalizeStatus$1(v1Control.status);
|
|
615
596
|
if (v1Control.results && Array.isArray(v1Control.results)) v2Req.results = v1Control.results.map(convertResult);
|
|
616
|
-
if (!v2Req.effectiveStatus) v2Req.effectiveStatus = computeEffectiveStatus(v1Control.impact, v2Req.results ?? []);
|
|
597
|
+
if (!v2Req.effectiveStatus) v2Req.effectiveStatus = computeEffectiveStatus$1(v1Control.impact, v2Req.results ?? []);
|
|
617
598
|
v2Req.severity = tagSeverityToSeverity(v1Control.tags?.severity) ?? impactToSeverity(v1Control.impact);
|
|
618
599
|
const rawNist = v1Control.tags?.nist;
|
|
619
600
|
if (Array.isArray(rawNist)) {
|
|
@@ -753,31 +734,47 @@ function convertProfile(v1Profile) {
|
|
|
753
734
|
if (v1Profile.depends?.length) v2Baseline.depends = v1Profile.depends.map(convertDependency);
|
|
754
735
|
return v2Baseline;
|
|
755
736
|
}
|
|
737
|
+
/** Parse the leading major-version integer; -1 when the string has none. */
|
|
738
|
+
function inspecMajor(version) {
|
|
739
|
+
const major = /^(\d+)/.exec(version ?? "")?.[1];
|
|
740
|
+
return major !== void 0 ? parseInt(major, 10) : -1;
|
|
741
|
+
}
|
|
756
742
|
/**
|
|
757
|
-
*
|
|
758
|
-
*
|
|
759
|
-
*
|
|
760
|
-
*
|
|
761
|
-
*
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
*
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
743
|
+
* Map the source's top-level version to tool metadata. A genuine InSpec
|
|
744
|
+
* exec-json run carries the InSpec CLI version (major >= 2 for every modern
|
|
745
|
+
* release), so those flip the tool to InSpec/exec-json. A legacy HDF v1
|
|
746
|
+
* document with no InSpec provenance (e.g. a bare "1.0.0" format marker) keeps
|
|
747
|
+
* the historical label.
|
|
748
|
+
*/
|
|
749
|
+
function toolIdentity(version) {
|
|
750
|
+
if (inspecMajor(version) >= 2) return {
|
|
751
|
+
name: "InSpec",
|
|
752
|
+
version,
|
|
753
|
+
format: "exec-json"
|
|
754
|
+
};
|
|
755
|
+
return { name: "Heimdall Data Format v1" };
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* The assessment's execution time for the top-level timestamp. An explicit
|
|
759
|
+
* source timestamp wins; otherwise it is the latest (last-observed) result
|
|
760
|
+
* start_time — the assessment's effective as-of instant, the value
|
|
761
|
+
* `hdf events derive` consumes. Returns undefined only when the source carries
|
|
762
|
+
* no usable time; never the wall clock. Canonical trimmed-UTC form.
|
|
763
|
+
*/
|
|
764
|
+
function documentTimestamp(v1Data) {
|
|
765
|
+
if (v1Data.timestamp) {
|
|
766
|
+
const t = parseTimestamp(v1Data.timestamp);
|
|
767
|
+
if (t) return formatTimestamp(t);
|
|
768
|
+
}
|
|
769
|
+
let latest;
|
|
770
|
+
for (const profile of v1Data.profiles || []) for (const control of profile.controls || []) for (const result of control.results || []) {
|
|
771
|
+
if (!result.start_time) continue;
|
|
772
|
+
const t = parseTimestamp(result.start_time);
|
|
773
|
+
if (t && (!latest || t > latest)) latest = t;
|
|
774
|
+
}
|
|
775
|
+
return latest ? formatTimestamp(latest) : void 0;
|
|
776
|
+
}
|
|
777
|
+
function convertV1ToV2(v1Data, converterVersion = "1.0.0") {
|
|
781
778
|
validateInputSize(JSON.stringify(v1Data), "legacyhdf-to-hdf");
|
|
782
779
|
const v2 = {
|
|
783
780
|
baselines: (v1Data.profiles || []).map(convertProfile),
|
|
@@ -794,9 +791,13 @@ function convertV1ToV2(v1Data) {
|
|
|
794
791
|
}
|
|
795
792
|
v2.components = [component];
|
|
796
793
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
794
|
+
v2.generator = v1Data.generator ?? {
|
|
795
|
+
name: "legacyhdf-to-hdf",
|
|
796
|
+
version: converterVersion
|
|
797
|
+
};
|
|
798
|
+
v2.tool = toolIdentity(v1Data.version);
|
|
799
|
+
const timestamp = documentTimestamp(v1Data);
|
|
800
|
+
if (timestamp) v2.timestamp = timestamp;
|
|
800
801
|
const knownV1Fields = /* @__PURE__ */ new Set([
|
|
801
802
|
"version",
|
|
802
803
|
"platform",
|
|
@@ -825,6 +826,63 @@ function isHDFV1(data) {
|
|
|
825
826
|
return typeof obj.version === "string" && Array.isArray(obj.profiles) && typeof obj.platform === "object" && obj.platform !== null;
|
|
826
827
|
}
|
|
827
828
|
//#endregion
|
|
829
|
+
//#region shared/typescript/cvss.ts
|
|
830
|
+
/**
|
|
831
|
+
* Returns the schema CVSS Version enum for a vector-string prefix (CVSS:2.0/,
|
|
832
|
+
* CVSS:3.0/, CVSS:4.0/). An absent or unrecognized prefix (including CVSS:3.1/)
|
|
833
|
+
* defaults to 3.1 — the version modern scanners emit most often.
|
|
834
|
+
*/
|
|
835
|
+
function cvssVersionFromVector(vector) {
|
|
836
|
+
if (!vector) return Version.The31;
|
|
837
|
+
if (vector.startsWith("CVSS:2.0/")) return Version.The20;
|
|
838
|
+
if (vector.startsWith("CVSS:3.0/")) return Version.The30;
|
|
839
|
+
if (vector.startsWith("CVSS:4.0/")) return Version.The40;
|
|
840
|
+
return Version.The31;
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Maps a bare CVSS version number ("2.0"/"3.0"/"3.1"/"4.0") — as emitted in a
|
|
844
|
+
* structured version field rather than a vector prefix — to the schema Version
|
|
845
|
+
* enum. Unrecognized values default to 3.1.
|
|
846
|
+
*/
|
|
847
|
+
function cvssVersionFromString(v) {
|
|
848
|
+
switch (v) {
|
|
849
|
+
case "2.0": return Version.The20;
|
|
850
|
+
case "3.0": return Version.The30;
|
|
851
|
+
case "4.0": return Version.The40;
|
|
852
|
+
default: return Version.The31;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Maps a CVSS base/computed score to the schema CVSSSeverity enum via the shared
|
|
857
|
+
* band thresholds in cvssScoreToSeverity. Scores below the low threshold (and
|
|
858
|
+
* non-finite inputs, per cvssScoreToSeverity) map to the "none" band.
|
|
859
|
+
*/
|
|
860
|
+
function cvssSeverityFromScore(score) {
|
|
861
|
+
switch (cvssScoreToSeverity(score)) {
|
|
862
|
+
case "critical": return CVSSSeverity.Critical;
|
|
863
|
+
case "high": return CVSSSeverity.High;
|
|
864
|
+
case "medium": return CVSSSeverity.Medium;
|
|
865
|
+
case "low": return CVSSSeverity.Low;
|
|
866
|
+
default: return CVSSSeverity.None;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Assembles a Cvss from the base-metric fields a converter provides. version is
|
|
871
|
+
* always set. baseScore (and its derived baseSeverity) is set only when a finite
|
|
872
|
+
* number is supplied; baseVector and source are set only when non-empty. Consumers
|
|
873
|
+
* needing threat/environmental/computed enrichment set those fields on the result.
|
|
874
|
+
*/
|
|
875
|
+
function buildCvss$2(input) {
|
|
876
|
+
const cv = { version: input.version };
|
|
877
|
+
if (typeof input.baseScore === "number" && Number.isFinite(input.baseScore)) {
|
|
878
|
+
cv.baseScore = input.baseScore;
|
|
879
|
+
cv.baseSeverity = cvssSeverityFromScore(input.baseScore);
|
|
880
|
+
}
|
|
881
|
+
if (input.baseVector) cv.baseVector = input.baseVector;
|
|
882
|
+
if (input.source) cv.source = input.source;
|
|
883
|
+
return cv;
|
|
884
|
+
}
|
|
885
|
+
//#endregion
|
|
828
886
|
//#region converters/asff-to-hdf/typescript/converter.ts
|
|
829
887
|
/**
|
|
830
888
|
* AWS Security Finding Format (ASFF) to HDF converter.
|
|
@@ -889,14 +947,17 @@ function severityLabelToImpact(label) {
|
|
|
889
947
|
/**
|
|
890
948
|
* findingImpact derives a 0.0–1.0 impact. Suppressed findings are forced to 0.
|
|
891
949
|
* Security Hub's INFORMATIONAL is up-graded to MEDIUM (Security Hub over-marks
|
|
892
|
-
* findings INFORMATIONAL without context).
|
|
950
|
+
* findings INFORMATIONAL without context). FindingProviderFields.Severity — the
|
|
951
|
+
* provider's own rating — is preferred over the top-level Severity, which
|
|
952
|
+
* Security Hub may overwrite on ingest.
|
|
893
953
|
*/
|
|
894
954
|
function findingImpact(f) {
|
|
895
955
|
if (f.Workflow?.Status === "SUPPRESSED") return 0;
|
|
896
|
-
|
|
956
|
+
const sev = f.FindingProviderFields?.Severity?.Label ? f.FindingProviderFields.Severity : f.Severity;
|
|
957
|
+
let label = sev?.Label;
|
|
897
958
|
if (isSecurityHub(f) && (label ?? "").toUpperCase() === "INFORMATIONAL") label = "MEDIUM";
|
|
898
959
|
if (label) return severityLabelToImpact(label);
|
|
899
|
-
if (typeof
|
|
960
|
+
if (typeof sev?.Normalized === "number") return sev.Normalized / 100;
|
|
900
961
|
return 0;
|
|
901
962
|
}
|
|
902
963
|
function productArnParts(arn) {
|
|
@@ -1061,6 +1122,37 @@ function vulnerabilitySummary(f) {
|
|
|
1061
1122
|
return parts.join("; ");
|
|
1062
1123
|
}).join("\n");
|
|
1063
1124
|
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Assembles requirement.cvss[] from a finding's ASFF Vulnerabilities[]. Each Cvss
|
|
1127
|
+
* entry carrying a BaseScore becomes one structured Cvss via the shared builder.
|
|
1128
|
+
* ASFF's Cvss shape has no vector string, so only version, score, and source map;
|
|
1129
|
+
* an entry without a BaseScore contributes nothing.
|
|
1130
|
+
*/
|
|
1131
|
+
function vulnCvss(f) {
|
|
1132
|
+
const out = [];
|
|
1133
|
+
for (const v of f.Vulnerabilities ?? []) for (const c of v.Cvss ?? []) {
|
|
1134
|
+
if (typeof c.BaseScore !== "number") continue;
|
|
1135
|
+
out.push(buildCvss$2({
|
|
1136
|
+
version: cvssVersionFromString(c.Version),
|
|
1137
|
+
baseScore: c.BaseScore,
|
|
1138
|
+
source: c.Source
|
|
1139
|
+
}));
|
|
1140
|
+
}
|
|
1141
|
+
return out;
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Collects the CVE ids a finding's Vulnerabilities[] carry (dedup, first-seen
|
|
1145
|
+
* order) for tags.cve. ASFF stores the CVE in Vulnerabilities[].Id.
|
|
1146
|
+
*/
|
|
1147
|
+
function vulnCves(f) {
|
|
1148
|
+
const out = [];
|
|
1149
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1150
|
+
for (const v of f.Vulnerabilities ?? []) if (v.Id && !seen.has(v.Id)) {
|
|
1151
|
+
seen.add(v.Id);
|
|
1152
|
+
out.push(v.Id);
|
|
1153
|
+
}
|
|
1154
|
+
return out;
|
|
1155
|
+
}
|
|
1064
1156
|
function trivyMessage(f) {
|
|
1065
1157
|
const o = f.Resources?.[0]?.Details?.Other;
|
|
1066
1158
|
if (!o) return "";
|
|
@@ -1100,10 +1192,15 @@ function buildRequirement$17(id, group) {
|
|
|
1100
1192
|
});
|
|
1101
1193
|
const nist = nistTags$1(group);
|
|
1102
1194
|
const tags = nist.length > 0 ? buildNistCciTags(nist, nistToCci(nist)) : {};
|
|
1195
|
+
const cves = vulnCves(primary);
|
|
1196
|
+
if (cves.length > 0) tags.cve = cves;
|
|
1103
1197
|
const results = group.map(buildResult$2);
|
|
1104
1198
|
const req = createRequirement(id, primary.Title ?? "", descriptions, impact, results, { tags });
|
|
1105
1199
|
const controlType = deriveControlTypeFromTags(nist);
|
|
1106
1200
|
if (controlType !== void 0) req.controlType = controlType;
|
|
1201
|
+
const cvss = vulnCvss(primary);
|
|
1202
|
+
if (cvss.length > 0) req.cvss = cvss;
|
|
1203
|
+
req.code = JSON.stringify(primary, null, 2);
|
|
1107
1204
|
const refs = [];
|
|
1108
1205
|
if (primary.SourceUrl) refs.push({ url: primary.SourceUrl });
|
|
1109
1206
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1159,10 +1256,7 @@ async function convertAsffToHdf(input, converterVersion = "1.0.0") {
|
|
|
1159
1256
|
name: "asff-to-hdf",
|
|
1160
1257
|
version: converterVersion
|
|
1161
1258
|
},
|
|
1162
|
-
tool: {
|
|
1163
|
-
name: "AWS Security Finding Format",
|
|
1164
|
-
format: "JSON"
|
|
1165
|
-
},
|
|
1259
|
+
tool: { name: "AWS Security Finding Format" },
|
|
1166
1260
|
timestamp: /* @__PURE__ */ new Date()
|
|
1167
1261
|
};
|
|
1168
1262
|
if (accounts.length > 0) {
|
|
@@ -1264,6 +1358,8 @@ function convertResultGroup(ruleId, rule, sarifResults) {
|
|
|
1264
1358
|
const controlType = deriveControlTypeFromTags(nistControls);
|
|
1265
1359
|
if (controlType !== void 0) req.controlType = controlType;
|
|
1266
1360
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
1361
|
+
const snippet = (firstResult.locations ?? []).map((loc) => loc.physicalLocation?.region?.snippet?.text).find((text) => Boolean(text));
|
|
1362
|
+
if (snippet) req.code = snippet;
|
|
1267
1363
|
const seenKeys = /* @__PURE__ */ new Set();
|
|
1268
1364
|
const packages = [];
|
|
1269
1365
|
for (const sr of sarifResults) {
|
|
@@ -1518,7 +1614,6 @@ async function convertJunitToHdf(input, converterVersion = "1.0.0") {
|
|
|
1518
1614
|
generatorName: "junit-to-hdf",
|
|
1519
1615
|
converterVersion,
|
|
1520
1616
|
toolName: "JUnit XML",
|
|
1521
|
-
toolFormat: "XML",
|
|
1522
1617
|
baselines: [createMinimalBaseline(name, requirements, { resultsChecksum: await inputChecksum(input) })],
|
|
1523
1618
|
components: [{
|
|
1524
1619
|
type: TargetType.Application,
|
|
@@ -1570,7 +1665,7 @@ function testCaseToRequirement(tc, scanTime) {
|
|
|
1570
1665
|
const { status, message } = resolveStatus(tc);
|
|
1571
1666
|
const result = {
|
|
1572
1667
|
status,
|
|
1573
|
-
codeDesc: buildCodeDesc$
|
|
1668
|
+
codeDesc: buildCodeDesc$10(tc),
|
|
1574
1669
|
startTime: scanTime
|
|
1575
1670
|
};
|
|
1576
1671
|
if (message !== void 0) result.message = message;
|
|
@@ -1627,7 +1722,7 @@ function buildFailureMessage(message, typeName, body) {
|
|
|
1627
1722
|
if (body) result += "\n" + decodeXmlEntities$5(body);
|
|
1628
1723
|
return result;
|
|
1629
1724
|
}
|
|
1630
|
-
function buildCodeDesc$
|
|
1725
|
+
function buildCodeDesc$10(tc) {
|
|
1631
1726
|
if (tc.classname) return `${decodeXmlEntities$5(tc.classname)} :: ${decodeXmlEntities$5(tc.name)}`;
|
|
1632
1727
|
return decodeXmlEntities$5(tc.name);
|
|
1633
1728
|
}
|
|
@@ -1639,6 +1734,25 @@ function noFindingsTarget(baselineName, suites) {
|
|
|
1639
1734
|
//#endregion
|
|
1640
1735
|
//#region converters/xccdf-results-to-hdf/typescript/converter.ts
|
|
1641
1736
|
/**
|
|
1737
|
+
* Map XCCDF's severity vocabulary (unknown|info|low|medium|high) onto HDF's
|
|
1738
|
+
* (critical|high|medium|low|informational). Casting the raw attribute straight to
|
|
1739
|
+
* Severity emitted schema-invalid documents, since XCCDF "unknown" and "info" are
|
|
1740
|
+
* not HDF severities.
|
|
1741
|
+
*
|
|
1742
|
+
* "unknown" states that severity was not determined. HDF cannot express that, so
|
|
1743
|
+
* emit no severity at all rather than assert "informational", which would silently
|
|
1744
|
+
* downgrade a rule whose severity is merely unstated.
|
|
1745
|
+
*/
|
|
1746
|
+
function xccdfSeverityToHdf(severity) {
|
|
1747
|
+
switch (severity.toLowerCase()) {
|
|
1748
|
+
case "high": return Severity.High;
|
|
1749
|
+
case "medium": return Severity.Medium;
|
|
1750
|
+
case "low": return Severity.Low;
|
|
1751
|
+
case "info": return Severity.Informational;
|
|
1752
|
+
default: return;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1642
1756
|
* Walk the Group tree depth-first, collecting every rule at any depth. Rules may
|
|
1643
1757
|
* sit directly on a Group that also has nested Groups.
|
|
1644
1758
|
*/
|
|
@@ -1883,6 +1997,12 @@ function ruleResultToRequirement(rr, ruleIndex, scanTime) {
|
|
|
1883
1997
|
label: "default",
|
|
1884
1998
|
data: stripHTML(extractVulnDiscussion(extractText(ruleDef?.description)))
|
|
1885
1999
|
}];
|
|
2000
|
+
const check = selectCheck(ruleDef?.check);
|
|
2001
|
+
const checkContent = extractCheckContent(check);
|
|
2002
|
+
if (checkContent) descriptions.push({
|
|
2003
|
+
label: "check",
|
|
2004
|
+
data: stripHTML(checkContent)
|
|
2005
|
+
});
|
|
1886
2006
|
const fixtext = extractFixtext(ruleDef?.fixtext);
|
|
1887
2007
|
if (fixtext) descriptions.push({
|
|
1888
2008
|
label: "fix",
|
|
@@ -1899,6 +2019,10 @@ function ruleResultToRequirement(rr, ruleIndex, scanTime) {
|
|
|
1899
2019
|
const tags = buildCciNistTags(extractCCIs([...rr.ident ?? [], ...ruleDef?.ident ?? []]));
|
|
1900
2020
|
const nistTags = tags["nist"];
|
|
1901
2021
|
const req = createRequirement(id, title, descriptions, impact, [result], { tags });
|
|
2022
|
+
const code = buildCheckCode(check);
|
|
2023
|
+
if (code) req.code = code;
|
|
2024
|
+
const hdfSeverity = xccdfSeverityToHdf(severity);
|
|
2025
|
+
if (hdfSeverity) req.severity = hdfSeverity;
|
|
1902
2026
|
const controlType = deriveControlTypeFromTags(nistTags);
|
|
1903
2027
|
if (controlType !== void 0) req.controlType = controlType;
|
|
1904
2028
|
return req;
|
|
@@ -1947,6 +2071,35 @@ function decodeXmlEntities$4(s) {
|
|
|
1947
2071
|
return name !== void 0 ? NAMED_ENTITIES$4[name] ?? match : match;
|
|
1948
2072
|
});
|
|
1949
2073
|
}
|
|
2074
|
+
/**
|
|
2075
|
+
* Extract plain text from a field that may be a string or {#text: string}.
|
|
2076
|
+
*/
|
|
2077
|
+
/**
|
|
2078
|
+
* A rule can carry several <check> elements (SSG emits SCE, OVAL and OCIL for the
|
|
2079
|
+
* same rule). Go decodes <check> into one struct field, so encoding/xml overwrites
|
|
2080
|
+
* it and the last element wins; match that or the two languages disagree.
|
|
2081
|
+
*/
|
|
2082
|
+
function checkPriority(system) {
|
|
2083
|
+
const s = system ?? "";
|
|
2084
|
+
if (s.includes("oval.mitre.org")) return 0;
|
|
2085
|
+
if (s.includes("open-scap.org") || s.includes("/SCE")) return 1;
|
|
2086
|
+
if (s.includes("ocil")) return 2;
|
|
2087
|
+
return 3;
|
|
2088
|
+
}
|
|
2089
|
+
function selectCheck(check) {
|
|
2090
|
+
if (check === void 0) return void 0;
|
|
2091
|
+
const checks = Array.isArray(check) ? check : [check];
|
|
2092
|
+
let best;
|
|
2093
|
+
let bestPri = Infinity;
|
|
2094
|
+
for (const c of checks) {
|
|
2095
|
+
const p = checkPriority(c.system);
|
|
2096
|
+
if (p < bestPri) {
|
|
2097
|
+
best = c;
|
|
2098
|
+
bestPri = p;
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
return best;
|
|
2102
|
+
}
|
|
1950
2103
|
function extractText(field) {
|
|
1951
2104
|
if (field === void 0 || field === null) return "";
|
|
1952
2105
|
if (typeof field === "string") return decodeXmlEntities$4(field);
|
|
@@ -1969,6 +2122,67 @@ function extractFixtext(field) {
|
|
|
1969
2122
|
return decodeXmlEntities$4(field["#text"] ?? "");
|
|
1970
2123
|
}
|
|
1971
2124
|
/**
|
|
2125
|
+
* Extract check-content text from a check element.
|
|
2126
|
+
*/
|
|
2127
|
+
function extractCheckContent(check) {
|
|
2128
|
+
if (!check) return "";
|
|
2129
|
+
const cc = check["check-content"];
|
|
2130
|
+
if (!cc) return "";
|
|
2131
|
+
if (typeof cc === "string") return decodeXmlEntities$4(cc);
|
|
2132
|
+
if (Array.isArray(cc)) {
|
|
2133
|
+
const first = cc[0];
|
|
2134
|
+
if (typeof first === "string") return decodeXmlEntities$4(first);
|
|
2135
|
+
if (first && typeof first === "object" && "#text" in first) return decodeXmlEntities$4(first["#text"] ?? "");
|
|
2136
|
+
return "";
|
|
2137
|
+
}
|
|
2138
|
+
return decodeXmlEntities$4(cc["#text"] ?? "");
|
|
2139
|
+
}
|
|
2140
|
+
/**
|
|
2141
|
+
* Extract the OVAL/SCE check-content-ref (name + href) from a check element.
|
|
2142
|
+
* check-content-ref is not forced into an array, so handle both single and
|
|
2143
|
+
* (rare) multi cases, taking the first.
|
|
2144
|
+
*/
|
|
2145
|
+
function extractCheckContentRef(check) {
|
|
2146
|
+
if (!check) return {
|
|
2147
|
+
name: "",
|
|
2148
|
+
href: ""
|
|
2149
|
+
};
|
|
2150
|
+
let ref = check["check-content-ref"];
|
|
2151
|
+
if (Array.isArray(ref)) ref = ref[0];
|
|
2152
|
+
if (!ref || typeof ref !== "object") return {
|
|
2153
|
+
name: "",
|
|
2154
|
+
href: ""
|
|
2155
|
+
};
|
|
2156
|
+
return {
|
|
2157
|
+
name: ref.name ?? "",
|
|
2158
|
+
href: ref.href ?? ""
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
/**
|
|
2162
|
+
* Render a rule's selected <check> as the indented-JSON blob carried in
|
|
2163
|
+
* requirement.code — the automated-check logic (system + OVAL/SCE definition
|
|
2164
|
+
* reference + any inline content). Returns '' when the check is empty so the
|
|
2165
|
+
* caller leaves code unset rather than fabricating one. Key order and the
|
|
2166
|
+
* escape-free JSON.stringify mirror the Go buildCheckCode for byte parity.
|
|
2167
|
+
*/
|
|
2168
|
+
function buildCheckCode(check) {
|
|
2169
|
+
if (!check) return "";
|
|
2170
|
+
const system = check.system ?? "";
|
|
2171
|
+
const ref = extractCheckContentRef(check);
|
|
2172
|
+
const content = extractCheckContent(check).trim();
|
|
2173
|
+
if (!system && !ref.name && !ref.href && !content) return "";
|
|
2174
|
+
const code = {};
|
|
2175
|
+
if (system) code.system = system;
|
|
2176
|
+
if (ref.name || ref.href) {
|
|
2177
|
+
const refObj = {};
|
|
2178
|
+
if (ref.name) refObj.name = ref.name;
|
|
2179
|
+
if (ref.href) refObj.href = ref.href;
|
|
2180
|
+
code.checkContentRef = refObj;
|
|
2181
|
+
}
|
|
2182
|
+
if (content) code.checkContent = content;
|
|
2183
|
+
return JSON.stringify(code, null, 2);
|
|
2184
|
+
}
|
|
2185
|
+
/**
|
|
1972
2186
|
* Extract the VulnDiscussion text from an XCCDF description that contains
|
|
1973
2187
|
* embedded XML like `<VulnDiscussion>...</VulnDiscussion>`. If no
|
|
1974
2188
|
* VulnDiscussion tag is found, returns the original text.
|
|
@@ -2783,6 +2997,19 @@ function synthesizePurl(ecosystem, name, version) {
|
|
|
2783
2997
|
if (ecosystem === Ecosystem.Generic) return void 0;
|
|
2784
2998
|
return `pkg:${ecosystem}/${name}@${version}`;
|
|
2785
2999
|
}
|
|
3000
|
+
/**
|
|
3001
|
+
* Assembles the structured cvss[] entry for a Snyk vulnerability from its
|
|
3002
|
+
* cvssScore (base score) and CVSSv3 (base vector, carrying a CVSS:3.1/ prefix).
|
|
3003
|
+
* Returns [] when the source carries neither so the field is omitted.
|
|
3004
|
+
*/
|
|
3005
|
+
function buildSnykCvss(vuln) {
|
|
3006
|
+
if (!vuln.cvssScore && !vuln.CVSSv3) return [];
|
|
3007
|
+
return [buildCvss$2({
|
|
3008
|
+
version: cvssVersionFromVector(vuln.CVSSv3),
|
|
3009
|
+
baseScore: vuln.cvssScore,
|
|
3010
|
+
baseVector: vuln.CVSSv3
|
|
3011
|
+
})];
|
|
3012
|
+
}
|
|
2786
3013
|
function buildRequirement$16(vulnID, vulns, scanTime, packageManager) {
|
|
2787
3014
|
const rep = vulns[0];
|
|
2788
3015
|
const cweIDs = rep.identifiers.CWE ?? [];
|
|
@@ -2791,8 +3018,7 @@ function buildRequirement$16(vulnID, vulns, scanTime, packageManager) {
|
|
|
2791
3018
|
nist,
|
|
2792
3019
|
cci: nistToCci(nist)
|
|
2793
3020
|
};
|
|
2794
|
-
if (
|
|
2795
|
-
if (rep.identifiers.CVE && rep.identifiers.CVE.length > 0) tags["cveid"] = rep.identifiers.CVE;
|
|
3021
|
+
if (rep.identifiers.CVE && rep.identifiers.CVE.length > 0) tags["cve"] = rep.identifiers.CVE;
|
|
2796
3022
|
if (rep.identifiers.GHSA && rep.identifiers.GHSA.length > 0) tags["ghsaid"] = rep.identifiers.GHSA;
|
|
2797
3023
|
const descriptions = [{
|
|
2798
3024
|
label: "default",
|
|
@@ -2810,6 +3036,9 @@ function buildRequirement$16(vulnID, vulns, scanTime, packageManager) {
|
|
|
2810
3036
|
const controlType = deriveControlTypeFromTags(nist);
|
|
2811
3037
|
if (controlType !== void 0) req.controlType = controlType;
|
|
2812
3038
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
3039
|
+
const cvss = buildSnykCvss(rep);
|
|
3040
|
+
if (cvss.length > 0) req.cvss = cvss;
|
|
3041
|
+
if (cweIDs.length > 0) req.cwe = cweIDs;
|
|
2813
3042
|
const name = rep.packageName ?? rep.moduleName;
|
|
2814
3043
|
const version = rep.version;
|
|
2815
3044
|
if (name && version) {
|
|
@@ -2885,7 +3114,6 @@ async function convertSnykToHdf(input, converterVersion = "1.0.0") {
|
|
|
2885
3114
|
generatorName: "snyk-to-hdf",
|
|
2886
3115
|
converterVersion,
|
|
2887
3116
|
toolName: "Snyk",
|
|
2888
|
-
toolFormat: "JSON",
|
|
2889
3117
|
baselines,
|
|
2890
3118
|
components: [{
|
|
2891
3119
|
name: targetName,
|
|
@@ -2944,7 +3172,7 @@ function getReferences(vuln, relatedVulns) {
|
|
|
2944
3172
|
}
|
|
2945
3173
|
return Array.from(refs);
|
|
2946
3174
|
}
|
|
2947
|
-
function buildCodeDesc$
|
|
3175
|
+
function buildCodeDesc$9(match) {
|
|
2948
3176
|
const parts = [];
|
|
2949
3177
|
parts.push(`Package: ${match.artifact.name}@${match.artifact.version}`);
|
|
2950
3178
|
if (match.artifact.type) parts.push(`Type: ${match.artifact.type}`);
|
|
@@ -2958,35 +3186,16 @@ function buildCodeDesc$8(match) {
|
|
|
2958
3186
|
}
|
|
2959
3187
|
return parts.join(" | ");
|
|
2960
3188
|
}
|
|
2961
|
-
function
|
|
2962
|
-
switch (v) {
|
|
2963
|
-
case "2.0": return Version.The20;
|
|
2964
|
-
case "3.0": return Version.The30;
|
|
2965
|
-
case "4.0": return Version.The40;
|
|
2966
|
-
default: return Version.The31;
|
|
2967
|
-
}
|
|
2968
|
-
}
|
|
2969
|
-
function cvssBandSeverity(score) {
|
|
2970
|
-
switch (cvssScoreToSeverity(score)) {
|
|
2971
|
-
case "critical": return CVSSSeverity.Critical;
|
|
2972
|
-
case "high": return CVSSSeverity.High;
|
|
2973
|
-
case "medium": return CVSSSeverity.Medium;
|
|
2974
|
-
case "low": return CVSSSeverity.Low;
|
|
2975
|
-
default: return CVSSSeverity.None;
|
|
2976
|
-
}
|
|
2977
|
-
}
|
|
2978
|
-
function buildCvssEntries$1(vuln) {
|
|
3189
|
+
function buildCvssEntries$5(vuln) {
|
|
2979
3190
|
if (!vuln.cvss || vuln.cvss.length === 0) return;
|
|
2980
3191
|
const entries = [];
|
|
2981
3192
|
for (const c of vuln.cvss) {
|
|
2982
|
-
const entry = {
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
}
|
|
2989
|
-
if (vuln.id) entry.source = vuln.id;
|
|
3193
|
+
const entry = buildCvss$2({
|
|
3194
|
+
version: cvssVersionFromString(c.version),
|
|
3195
|
+
baseScore: c.metrics?.baseScore,
|
|
3196
|
+
baseVector: c.vector,
|
|
3197
|
+
source: vuln.id
|
|
3198
|
+
});
|
|
2990
3199
|
if (entry.baseVector === void 0 && entry.baseScore === void 0) continue;
|
|
2991
3200
|
entries.push(entry);
|
|
2992
3201
|
}
|
|
@@ -3020,10 +3229,10 @@ function buildAffectedPackages(match) {
|
|
|
3020
3229
|
if (fix && fix.state === "fixed" && fix.versions && fix.versions.length > 0 && fix.versions[0]) pkg.fixedInVersion = fix.versions[0];
|
|
3021
3230
|
return [pkg];
|
|
3022
3231
|
}
|
|
3023
|
-
const CWE_ID_PATTERN = /^CWE-[1-9]\d*$/;
|
|
3232
|
+
const CWE_ID_PATTERN$1 = /^CWE-[1-9]\d*$/;
|
|
3024
3233
|
function extractCwe(raw) {
|
|
3025
3234
|
if (!raw || raw.length === 0) return void 0;
|
|
3026
|
-
const out = raw.filter((c) => CWE_ID_PATTERN.test(c));
|
|
3235
|
+
const out = raw.filter((c) => CWE_ID_PATTERN$1.test(c));
|
|
3027
3236
|
return out.length === 0 ? void 0 : out;
|
|
3028
3237
|
}
|
|
3029
3238
|
function buildEpss$2(entries) {
|
|
@@ -3044,7 +3253,7 @@ function buildKev(k) {
|
|
|
3044
3253
|
if (k.notes) out.notes = k.notes;
|
|
3045
3254
|
return out;
|
|
3046
3255
|
}
|
|
3047
|
-
function convertMatchToRequirement(match, isIgnored) {
|
|
3256
|
+
function convertMatchToRequirement(match, isIgnored, targetName, startTime) {
|
|
3048
3257
|
const vuln = match.vulnerability;
|
|
3049
3258
|
const cveId = vuln.id;
|
|
3050
3259
|
const severity = vuln.severity;
|
|
@@ -3065,14 +3274,16 @@ function convertMatchToRequirement(match, isIgnored) {
|
|
|
3065
3274
|
const message = messageParts.join(" ");
|
|
3066
3275
|
const result = {
|
|
3067
3276
|
status,
|
|
3068
|
-
codeDesc: buildCodeDesc$
|
|
3277
|
+
codeDesc: buildCodeDesc$9(match),
|
|
3069
3278
|
message,
|
|
3070
|
-
startTime
|
|
3279
|
+
startTime
|
|
3071
3280
|
};
|
|
3072
3281
|
const tags = buildNistCciTags(DEFAULT_STATIC_ANALYSIS_NIST_TAGS, nistToCci(DEFAULT_STATIC_ANALYSIS_NIST_TAGS));
|
|
3073
3282
|
const requirement = {
|
|
3074
3283
|
id: isIgnored ? `Grype-Ignored-Match/${cveId}` : `Grype/${cveId}`,
|
|
3284
|
+
title: `Grype found a vulnerability to ${cveId} in ${targetName}`,
|
|
3075
3285
|
impact,
|
|
3286
|
+
code: JSON.stringify(match, null, 2),
|
|
3076
3287
|
results: [result],
|
|
3077
3288
|
tags,
|
|
3078
3289
|
descriptions: [
|
|
@@ -3094,7 +3305,7 @@ function convertMatchToRequirement(match, isIgnored) {
|
|
|
3094
3305
|
};
|
|
3095
3306
|
const controlType = deriveControlTypeFromTags(DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
|
|
3096
3307
|
if (controlType !== void 0) requirement.controlType = controlType;
|
|
3097
|
-
const cvss = buildCvssEntries$
|
|
3308
|
+
const cvss = buildCvssEntries$5(vuln);
|
|
3098
3309
|
if (cvss) requirement.cvss = cvss;
|
|
3099
3310
|
requirement.affectedPackages = buildAffectedPackages(match);
|
|
3100
3311
|
const cwe = extractCwe(vuln.cwe);
|
|
@@ -3110,19 +3321,21 @@ async function convertGrypeToHdf(input, converterVersion = "1.0.0") {
|
|
|
3110
3321
|
const resultsChecksum = await inputChecksum(input);
|
|
3111
3322
|
const grypeData = parseJSON(input);
|
|
3112
3323
|
const requirements = [];
|
|
3324
|
+
const targetName = grypeData.source?.target?.userInput || "Grype Scan";
|
|
3325
|
+
const scanTime = grypeData.descriptor?.timestamp ? parseTimestamp(grypeData.descriptor.timestamp) : null;
|
|
3326
|
+
const resultStart = scanTime ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z");
|
|
3113
3327
|
if (grypeData.matches && grypeData.matches.length > 0) {
|
|
3114
3328
|
const { items: limitedMatches, truncated: truncatedMatches } = limitArray(grypeData.matches);
|
|
3115
3329
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
3116
3330
|
if (truncatedMatches) console.warn(`WARNING: Input truncated at ${limitedMatches.length} match items (original: ${grypeData.matches.length})`);
|
|
3117
|
-
for (const match of limitedMatches) requirements.push(convertMatchToRequirement(match, false));
|
|
3331
|
+
for (const match of limitedMatches) requirements.push(convertMatchToRequirement(match, false, targetName, resultStart));
|
|
3118
3332
|
}
|
|
3119
3333
|
if (grypeData.ignoredMatches && grypeData.ignoredMatches.length > 0) {
|
|
3120
3334
|
const { items: limitedIgnored, truncated: truncatedIgnored } = limitArray(grypeData.ignoredMatches);
|
|
3121
3335
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
3122
3336
|
if (truncatedIgnored) console.warn(`WARNING: Input truncated at ${limitedIgnored.length} ignoredMatch items (original: ${grypeData.ignoredMatches.length})`);
|
|
3123
|
-
for (const match of limitedIgnored) requirements.push(convertMatchToRequirement(match, true));
|
|
3337
|
+
for (const match of limitedIgnored) requirements.push(convertMatchToRequirement(match, true, targetName, resultStart));
|
|
3124
3338
|
}
|
|
3125
|
-
const targetName = grypeData.source?.target?.userInput || "Grype Scan";
|
|
3126
3339
|
if (requirements.length === 0) requirements.push(buildNoFindingsRequirement("grype-no-findings", `Grype scanned ${targetName} and reported zero vulnerable components.`, /* @__PURE__ */ new Date()));
|
|
3127
3340
|
const baseline = createMinimalBaseline(targetName, requirements, { resultsChecksum });
|
|
3128
3341
|
return buildHdfResults({
|
|
@@ -3135,7 +3348,7 @@ async function convertGrypeToHdf(input, converterVersion = "1.0.0") {
|
|
|
3135
3348
|
type: TargetType.Artifact,
|
|
3136
3349
|
name: targetName
|
|
3137
3350
|
}],
|
|
3138
|
-
timestamp:
|
|
3351
|
+
timestamp: scanTime ?? void 0
|
|
3139
3352
|
});
|
|
3140
3353
|
}
|
|
3141
3354
|
//#endregion
|
|
@@ -3200,26 +3413,15 @@ function findingId(f) {
|
|
|
3200
3413
|
function cveList(f) {
|
|
3201
3414
|
return (f.vulnerability_ids ?? []).map((v) => v.vulnerability_id).filter(Boolean);
|
|
3202
3415
|
}
|
|
3203
|
-
function cvssBand(score) {
|
|
3204
|
-
switch (cvssScoreToSeverity(score)) {
|
|
3205
|
-
case "critical": return CVSSSeverity.Critical;
|
|
3206
|
-
case "high": return CVSSSeverity.High;
|
|
3207
|
-
case "medium": return CVSSSeverity.Medium;
|
|
3208
|
-
case "low": return CVSSSeverity.Low;
|
|
3209
|
-
default: return CVSSSeverity.None;
|
|
3210
|
-
}
|
|
3211
|
-
}
|
|
3212
3416
|
function buildCvss$1(f) {
|
|
3213
3417
|
const out = [];
|
|
3214
3418
|
const add = (version, vector, score) => {
|
|
3215
3419
|
if ((score === void 0 || score === null) && !vector) return;
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
}
|
|
3221
|
-
if (vector) entry.baseVector = vector;
|
|
3222
|
-
out.push(entry);
|
|
3420
|
+
out.push(buildCvss$2({
|
|
3421
|
+
version,
|
|
3422
|
+
baseScore: score,
|
|
3423
|
+
baseVector: vector
|
|
3424
|
+
}));
|
|
3223
3425
|
};
|
|
3224
3426
|
add(Version.The31, f.cvssv3, f.cvssv3_score);
|
|
3225
3427
|
add(Version.The40, f.cvssv4, f.cvssv4_score);
|
|
@@ -3262,6 +3464,8 @@ function codeDesc(f) {
|
|
|
3262
3464
|
function convertFinding(f) {
|
|
3263
3465
|
const nist = nistTags(f);
|
|
3264
3466
|
const tags = buildNistCciTags(nist, nistToCci(nist), triageTags(f));
|
|
3467
|
+
const cves = cveList(f);
|
|
3468
|
+
if (cves.length > 0) tags.cve = cves;
|
|
3265
3469
|
const result = {
|
|
3266
3470
|
status: deriveStatus(f),
|
|
3267
3471
|
codeDesc: codeDesc(f),
|
|
@@ -3281,6 +3485,7 @@ function convertFinding(f) {
|
|
|
3281
3485
|
const epss = buildEpss$1(f);
|
|
3282
3486
|
if (epss) req.epss = epss;
|
|
3283
3487
|
if (f.cwe && f.cwe > 0) req.cwe = [`CWE-${f.cwe}`];
|
|
3488
|
+
req.code = JSON.stringify(f, null, 2);
|
|
3284
3489
|
const firstRisk = f.accepted_risks?.[0];
|
|
3285
3490
|
if (f.risk_accepted && firstRisk) {
|
|
3286
3491
|
req.statusOverrides = [buildWaiverOverride(firstRisk)];
|
|
@@ -3489,7 +3694,7 @@ function convertReportItemToRequirement(item, host) {
|
|
|
3489
3694
|
if (controlType !== void 0) req.controlType = controlType;
|
|
3490
3695
|
if (verificationMethod !== void 0) req.verificationMethod = verificationMethod;
|
|
3491
3696
|
if (!isCompliance) {
|
|
3492
|
-
const cvssEntries = buildCvssEntries(item);
|
|
3697
|
+
const cvssEntries = buildCvssEntries$4(item);
|
|
3493
3698
|
if (cvssEntries.length > 0) req.cvss = cvssEntries;
|
|
3494
3699
|
const cweIDs = buildCweIDs(item);
|
|
3495
3700
|
if (cweIDs.length > 0) req.cwe = cweIDs;
|
|
@@ -3503,7 +3708,7 @@ function convertReportItemToRequirement(item, host) {
|
|
|
3503
3708
|
* the schema models cvss as a multi-entry array (Nessus emits one entry per
|
|
3504
3709
|
* item; multi-vendor convergence may yield more).
|
|
3505
3710
|
*/
|
|
3506
|
-
function buildCvssEntries(item) {
|
|
3711
|
+
function buildCvssEntries$4(item) {
|
|
3507
3712
|
const source = (item.cvss_score_source || "").trim();
|
|
3508
3713
|
if (!source || !CVE_SOURCE_RE.test(source)) return [];
|
|
3509
3714
|
const hasV3 = !!(item.cvss3_vector || item.cvss3_base_score);
|
|
@@ -3534,15 +3739,13 @@ function buildCvssEntries(item) {
|
|
|
3534
3739
|
if (baseVector) entry.baseVector = baseVector;
|
|
3535
3740
|
if (baseScore !== void 0) {
|
|
3536
3741
|
entry.baseScore = baseScore;
|
|
3537
|
-
|
|
3538
|
-
if (baseSeverity !== void 0) entry.baseSeverity = baseSeverity;
|
|
3742
|
+
entry.baseSeverity = cvssSeverityFromScore(baseScore);
|
|
3539
3743
|
}
|
|
3540
3744
|
if (threatVector !== void 0 && threatVector !== "") entry.threatVector = threatVector;
|
|
3541
3745
|
if (threatScore !== void 0) {
|
|
3542
3746
|
entry.threatScore = threatScore;
|
|
3543
3747
|
entry.computedScore = threatScore;
|
|
3544
|
-
|
|
3545
|
-
if (computedSeverity !== void 0) entry.computedSeverity = computedSeverity;
|
|
3748
|
+
entry.computedSeverity = cvssSeverityFromScore(threatScore);
|
|
3546
3749
|
}
|
|
3547
3750
|
return [entry];
|
|
3548
3751
|
}
|
|
@@ -3573,16 +3776,6 @@ function parseFloatOrUndef(s) {
|
|
|
3573
3776
|
const f = Number.parseFloat(s);
|
|
3574
3777
|
return Number.isFinite(f) ? f : void 0;
|
|
3575
3778
|
}
|
|
3576
|
-
function mapCvssSeverity(score) {
|
|
3577
|
-
switch (cvssScoreToSeverity(score)) {
|
|
3578
|
-
case "critical": return CVSSSeverity.Critical;
|
|
3579
|
-
case "high": return CVSSSeverity.High;
|
|
3580
|
-
case "medium": return CVSSSeverity.Medium;
|
|
3581
|
-
case "low": return CVSSSeverity.Low;
|
|
3582
|
-
case "none": return CVSSSeverity.None;
|
|
3583
|
-
default: return;
|
|
3584
|
-
}
|
|
3585
|
-
}
|
|
3586
3779
|
/**
|
|
3587
3780
|
* Extract CWE IDs from a ReportItem's <cwe> elements. Nessus emits bare
|
|
3588
3781
|
* numeric IDs (e.g. <cwe>200</cwe>); occasionally pipe-separated or prefixed
|
|
@@ -4069,7 +4262,7 @@ function buildCheckText(rule) {
|
|
|
4069
4262
|
}
|
|
4070
4263
|
return parts.join("<br/>");
|
|
4071
4264
|
}
|
|
4072
|
-
function buildCodeDesc$
|
|
4265
|
+
function buildCodeDesc$8(q) {
|
|
4073
4266
|
return `config_rule_name: ${q.ConfigRuleName}, resource_type: ${q.ResourceType}, resource_id: ${q.ResourceId}`;
|
|
4074
4267
|
}
|
|
4075
4268
|
function buildResultMessage(codeDesc, annotation, status) {
|
|
@@ -4086,7 +4279,7 @@ function computeRunTime(invoked, recorded) {
|
|
|
4086
4279
|
function buildResult(r) {
|
|
4087
4280
|
const q = r.EvaluationResultIdentifier.EvaluationResultQualifier;
|
|
4088
4281
|
const status = mapComplianceStatus(r.ComplianceType);
|
|
4089
|
-
const codeDesc = buildCodeDesc$
|
|
4282
|
+
const codeDesc = buildCodeDesc$8(q);
|
|
4090
4283
|
return createResult(status, buildResultMessage(codeDesc, r.Annotation, status), {
|
|
4091
4284
|
codeDesc,
|
|
4092
4285
|
startTime: (r.ConfigRuleInvokedTime ? parseTimestamp(r.ConfigRuleInvokedTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z"),
|
|
@@ -4214,12 +4407,32 @@ function checkToResult(check, scanTime) {
|
|
|
4214
4407
|
});
|
|
4215
4408
|
}
|
|
4216
4409
|
/**
|
|
4410
|
+
* Renders checkov's code_block ([[lineno, "source"], ...]) into a readable,
|
|
4411
|
+
* line-numbered source snippet for the Heimdall CODE tab. Returns undefined when
|
|
4412
|
+
* the code_block is absent or not an array so requirement.code is omitted.
|
|
4413
|
+
*/
|
|
4414
|
+
function renderCodeBlock(codeBlock) {
|
|
4415
|
+
if (!Array.isArray(codeBlock)) return void 0;
|
|
4416
|
+
const lines = [];
|
|
4417
|
+
for (const entry of codeBlock) {
|
|
4418
|
+
if (!Array.isArray(entry) || entry.length < 2) continue;
|
|
4419
|
+
const lineno = entry[0];
|
|
4420
|
+
const src = typeof entry[1] === "string" ? entry[1] : "";
|
|
4421
|
+
const trimmed = src.endsWith("\n") ? src.slice(0, -1) : src;
|
|
4422
|
+
lines.push(`${lineno} ${trimmed}`);
|
|
4423
|
+
}
|
|
4424
|
+
return lines.length ? lines.join("\n") : void 0;
|
|
4425
|
+
}
|
|
4426
|
+
/**
|
|
4217
4427
|
* Converts a group of checks sharing a check_id into one EvaluatedRequirement.
|
|
4218
4428
|
*/
|
|
4219
|
-
function buildRequirement$14(checkId,
|
|
4429
|
+
function buildRequirement$14(checkId, group, scanTime) {
|
|
4430
|
+
const checks = group.map((c) => c.check);
|
|
4220
4431
|
const rep = checks[0];
|
|
4221
4432
|
const impact = getImpact$8(rep.severity);
|
|
4222
4433
|
const tags = { nist: [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS] };
|
|
4434
|
+
const checkTypes = [...new Set(group.map((c) => c.checkType).filter((t) => t !== ""))].sort();
|
|
4435
|
+
if (checkTypes.length > 0) tags["check_type"] = checkTypes;
|
|
4223
4436
|
const descriptions = [{
|
|
4224
4437
|
label: "default",
|
|
4225
4438
|
data: rep.check_name
|
|
@@ -4231,6 +4444,8 @@ function buildRequirement$14(checkId, checks, scanTime) {
|
|
|
4231
4444
|
const results = checks.map((check) => checkToResult(check, scanTime));
|
|
4232
4445
|
const req = createRequirement(checkId, rep.check_name, descriptions, impact, results, { tags });
|
|
4233
4446
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
4447
|
+
const code = renderCodeBlock(rep.code_block);
|
|
4448
|
+
if (code !== void 0) req.code = code;
|
|
4234
4449
|
const controlType = deriveControlTypeFromTags([...DEFAULT_STATIC_ANALYSIS_NIST_TAGS]);
|
|
4235
4450
|
if (controlType !== void 0) req.controlType = controlType;
|
|
4236
4451
|
return req;
|
|
@@ -4267,18 +4482,23 @@ async function convertCheckovToHdf(input, converterVersion = "1.0.0") {
|
|
|
4267
4482
|
for (const report of reports) {
|
|
4268
4483
|
checkTypes.push(report.check_type);
|
|
4269
4484
|
if (!version && report.summary.checkov_version) version = report.summary.checkov_version;
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4485
|
+
for (const check of [
|
|
4486
|
+
...report.results.passed_checks,
|
|
4487
|
+
...report.results.failed_checks,
|
|
4488
|
+
...report.results.skipped_checks
|
|
4489
|
+
]) allChecks.push({
|
|
4490
|
+
check,
|
|
4491
|
+
checkType: report.check_type
|
|
4492
|
+
});
|
|
4273
4493
|
}
|
|
4274
4494
|
const { items: limitedChecks, truncated } = limitArray(allChecks);
|
|
4275
4495
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
4276
4496
|
if (truncated) console.warn(`WARNING: Input truncated at ${limitedChecks.length} check items (original: ${allChecks.length})`);
|
|
4277
4497
|
const groups = /* @__PURE__ */ new Map();
|
|
4278
|
-
for (const
|
|
4279
|
-
const existing = groups.get(check.check_id);
|
|
4280
|
-
if (existing) existing.push(
|
|
4281
|
-
else groups.set(check.check_id, [
|
|
4498
|
+
for (const entry of limitedChecks) {
|
|
4499
|
+
const existing = groups.get(entry.check.check_id);
|
|
4500
|
+
if (existing) existing.push(entry);
|
|
4501
|
+
else groups.set(entry.check.check_id, [entry]);
|
|
4282
4502
|
}
|
|
4283
4503
|
const requirements = [];
|
|
4284
4504
|
for (const [checkId, checks] of groups) requirements.push(buildRequirement$14(checkId, checks, scanTime));
|
|
@@ -4293,7 +4513,6 @@ async function convertCheckovToHdf(input, converterVersion = "1.0.0") {
|
|
|
4293
4513
|
converterVersion,
|
|
4294
4514
|
toolName: "Checkov",
|
|
4295
4515
|
toolVersion: version,
|
|
4296
|
-
toolFormat: format,
|
|
4297
4516
|
baselines: [baseline],
|
|
4298
4517
|
timestamp: scanTime
|
|
4299
4518
|
});
|
|
@@ -4309,6 +4528,13 @@ const IMPACT_MAPPING$6 = {
|
|
|
4309
4528
|
LOW: .3
|
|
4310
4529
|
};
|
|
4311
4530
|
/**
|
|
4531
|
+
* Returns the first-class CWE identifiers for an issue in "CWE-N" form,
|
|
4532
|
+
* or an empty array when the issue carries no CWE.
|
|
4533
|
+
*/
|
|
4534
|
+
function cweIds(issue) {
|
|
4535
|
+
return issue.cwe.id ? [`CWE-${issue.cwe.id}`] : [];
|
|
4536
|
+
}
|
|
4537
|
+
/**
|
|
4312
4538
|
* Returns true if the issue is suppressed (via nosec flag or suppressions list).
|
|
4313
4539
|
*/
|
|
4314
4540
|
function isSuppressed(issue) {
|
|
@@ -4345,13 +4571,7 @@ function buildRequirement$13(ruleId, issues, scanTime) {
|
|
|
4345
4571
|
const rep = issues[0];
|
|
4346
4572
|
const impact = IMPACT_MAPPING$6[rep.severity.toUpperCase()] ?? .5;
|
|
4347
4573
|
const nist = mapCWEToNIST([rep.cwe.id], DEFAULT_REMEDIATION_NIST_TAGS$1);
|
|
4348
|
-
const tags = {
|
|
4349
|
-
nist,
|
|
4350
|
-
cwe: {
|
|
4351
|
-
id: rep.cwe.id,
|
|
4352
|
-
url: rep.cwe.url
|
|
4353
|
-
}
|
|
4354
|
-
};
|
|
4574
|
+
const tags = { nist };
|
|
4355
4575
|
const results = issues.map((issue) => issueToResult(issue, scanTime));
|
|
4356
4576
|
const descriptions = [{
|
|
4357
4577
|
label: "default",
|
|
@@ -4361,6 +4581,9 @@ function buildRequirement$13(ruleId, issues, scanTime) {
|
|
|
4361
4581
|
data: `CWE-${rep.cwe.id}: ${rep.cwe.url}`
|
|
4362
4582
|
}];
|
|
4363
4583
|
const req = createRequirement(ruleId, rep.details, descriptions, impact, results, { tags });
|
|
4584
|
+
const cwe = cweIds(rep);
|
|
4585
|
+
if (cwe.length > 0) req.cwe = cwe;
|
|
4586
|
+
req.code = rep.code;
|
|
4364
4587
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
4365
4588
|
const controlType = deriveControlTypeFromTags(nist);
|
|
4366
4589
|
if (controlType !== void 0) req.controlType = controlType;
|
|
@@ -4413,7 +4636,7 @@ function buildNistTags$2(niktoId) {
|
|
|
4413
4636
|
if (control) return [control];
|
|
4414
4637
|
return [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS];
|
|
4415
4638
|
}
|
|
4416
|
-
function buildCodeDesc$
|
|
4639
|
+
function buildCodeDesc$7(vuln) {
|
|
4417
4640
|
const parts = [];
|
|
4418
4641
|
if (vuln.url) parts.push(`URL: ${vuln.url}`);
|
|
4419
4642
|
if (vuln.method) parts.push(`Method: ${vuln.method}`);
|
|
@@ -4424,7 +4647,7 @@ function convertVulnToRequirement(vuln) {
|
|
|
4424
4647
|
const cciTags = nistToCci(nistTags);
|
|
4425
4648
|
const result = {
|
|
4426
4649
|
status: ResultStatus.Failed,
|
|
4427
|
-
codeDesc: buildCodeDesc$
|
|
4650
|
+
codeDesc: buildCodeDesc$7(vuln),
|
|
4428
4651
|
startTime: /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
|
|
4429
4652
|
};
|
|
4430
4653
|
const extras = {};
|
|
@@ -4467,7 +4690,7 @@ async function convertNiktoToHdf(input, converterVersion = "unknown") {
|
|
|
4467
4690
|
const req = convertVulnToRequirement(primary);
|
|
4468
4691
|
if (vulns.length > 1) for (let i = 1; i < vulns.length; i++) req.results.push({
|
|
4469
4692
|
status: ResultStatus.Failed,
|
|
4470
|
-
codeDesc: buildCodeDesc$
|
|
4693
|
+
codeDesc: buildCodeDesc$7(vulns[i]),
|
|
4471
4694
|
startTime: /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
|
|
4472
4695
|
});
|
|
4473
4696
|
requirements.push(req);
|
|
@@ -4481,7 +4704,6 @@ async function convertNiktoToHdf(input, converterVersion = "unknown") {
|
|
|
4481
4704
|
generatorName: "nikto-to-hdf",
|
|
4482
4705
|
converterVersion,
|
|
4483
4706
|
toolName: "Nikto",
|
|
4484
|
-
toolFormat: "JSON",
|
|
4485
4707
|
baselines: [createMinimalBaseline(targetName, requirements, {
|
|
4486
4708
|
resultsChecksum,
|
|
4487
4709
|
title: `Nikto Target: ${targetName}`,
|
|
@@ -4514,7 +4736,13 @@ function buildNistTags$1(cweid) {
|
|
|
4514
4736
|
}
|
|
4515
4737
|
return [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS];
|
|
4516
4738
|
}
|
|
4517
|
-
function
|
|
4739
|
+
function buildCwe(cweid) {
|
|
4740
|
+
if (!cweid || cweid === "0") return;
|
|
4741
|
+
const cweNum = parseInt(cweid, 10);
|
|
4742
|
+
if (isNaN(cweNum)) return;
|
|
4743
|
+
return [`CWE-${cweNum}`];
|
|
4744
|
+
}
|
|
4745
|
+
function buildCodeDesc$6(instance) {
|
|
4518
4746
|
const parts = [];
|
|
4519
4747
|
if (instance.uri) parts.push(`URI: ${instance.uri}`);
|
|
4520
4748
|
if (instance.method) parts.push(`Method: ${instance.method}`);
|
|
@@ -4522,6 +4750,21 @@ function buildCodeDesc$5(instance) {
|
|
|
4522
4750
|
if (instance.evidence) parts.push(`Evidence: ${instance.evidence}`);
|
|
4523
4751
|
return parts.join(" | ");
|
|
4524
4752
|
}
|
|
4753
|
+
function representativeInstance(instances) {
|
|
4754
|
+
if (!instances || instances.length === 0) return;
|
|
4755
|
+
return instances.find((inst) => inst.attack) ?? instances[0];
|
|
4756
|
+
}
|
|
4757
|
+
function buildRequirementCode$1(alert) {
|
|
4758
|
+
const inst = representativeInstance(alert.instances);
|
|
4759
|
+
if (!inst) return;
|
|
4760
|
+
const parts = [];
|
|
4761
|
+
const requestLine = `${inst.method ?? ""} ${inst.uri ?? ""}`.trim();
|
|
4762
|
+
if (requestLine) parts.push(requestLine);
|
|
4763
|
+
if (inst.param) parts.push(`Param: ${inst.param}`);
|
|
4764
|
+
if (inst.attack) parts.push(`Attack: ${inst.attack}`);
|
|
4765
|
+
if (parts.length === 0) return;
|
|
4766
|
+
return parts.join("\n");
|
|
4767
|
+
}
|
|
4525
4768
|
function buildCheckDescription(alert) {
|
|
4526
4769
|
const parts = [];
|
|
4527
4770
|
if (alert.solution) {
|
|
@@ -4534,33 +4777,11 @@ function buildCheckDescription(alert) {
|
|
|
4534
4777
|
}
|
|
4535
4778
|
return parts.join("\n");
|
|
4536
4779
|
}
|
|
4537
|
-
function
|
|
4538
|
-
|
|
4539
|
-
let best = sites[0];
|
|
4540
|
-
for (let i = 1; i < sites.length; i++) {
|
|
4541
|
-
const site = sites[i];
|
|
4542
|
-
if ((site.alerts?.length ?? 0) > (best.alerts?.length ?? 0)) best = site;
|
|
4543
|
-
}
|
|
4544
|
-
return best;
|
|
4545
|
-
}
|
|
4546
|
-
const ZAP_RFC1123_LIKE = /^[A-Za-z]{3}, \d{1,2} [A-Za-z]{3} \d{4} \d{2}:\d{2}:\d{2}$/;
|
|
4547
|
-
function parseZapTimestamp(s) {
|
|
4548
|
-
const trimmed = s.trim();
|
|
4549
|
-
if (ZAP_RFC1123_LIKE.test(trimmed)) {
|
|
4550
|
-
const d = /* @__PURE__ */ new Date(`${trimmed} GMT`);
|
|
4551
|
-
if (!isNaN(d.getTime())) return d;
|
|
4552
|
-
}
|
|
4553
|
-
return parseTimestamp(s) ?? void 0;
|
|
4780
|
+
function siteLabel(site, index) {
|
|
4781
|
+
return site["@host"] || site["@name"] || `site ${index + 1}`;
|
|
4554
4782
|
}
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
registerAllFingerprints();
|
|
4558
|
-
const detected = detectConverter(input);
|
|
4559
|
-
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
|
|
4560
|
-
const resultsChecksum = await inputChecksum(input);
|
|
4561
|
-
const zapData = parseJSON(input);
|
|
4562
|
-
const site = selectSite(Array.isArray(zapData.site) ? zapData.site : []);
|
|
4563
|
-
const alerts = site?.alerts ?? [];
|
|
4783
|
+
function buildSiteRequirements(site) {
|
|
4784
|
+
const alerts = site.alerts ?? [];
|
|
4564
4785
|
const pluginIdCount = /* @__PURE__ */ new Map();
|
|
4565
4786
|
const { items: limitedAlerts, truncated } = limitArray(alerts);
|
|
4566
4787
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
@@ -4573,7 +4794,6 @@ async function convertZapToHdf(input, converterVersion = "1.0.0") {
|
|
|
4573
4794
|
const nistTags = buildNistTags$1(alert.cweid ?? "");
|
|
4574
4795
|
const cciTags = nistToCci(nistTags);
|
|
4575
4796
|
const extras = {};
|
|
4576
|
-
if (alert.cweid) extras.cweid = alert.cweid;
|
|
4577
4797
|
if (alert.wascid) extras.wascid = alert.wascid;
|
|
4578
4798
|
if (alert.riskdesc) extras.riskdesc = alert.riskdesc;
|
|
4579
4799
|
if (alert.confidence) extras.confidence = alert.confidence;
|
|
@@ -4585,7 +4805,7 @@ async function convertZapToHdf(input, converterVersion = "1.0.0") {
|
|
|
4585
4805
|
for (const instance of limitedInstances) {
|
|
4586
4806
|
const result = {
|
|
4587
4807
|
status: ResultStatus.Failed,
|
|
4588
|
-
codeDesc: buildCodeDesc$
|
|
4808
|
+
codeDesc: buildCodeDesc$6(instance),
|
|
4589
4809
|
startTime: /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
|
|
4590
4810
|
};
|
|
4591
4811
|
if (instance.attack) result.message = instance.attack;
|
|
@@ -4612,40 +4832,76 @@ async function convertZapToHdf(input, converterVersion = "1.0.0") {
|
|
|
4612
4832
|
descriptions,
|
|
4613
4833
|
verificationMethod: VerificationMethodEnum.Automated
|
|
4614
4834
|
};
|
|
4835
|
+
const cwe = buildCwe(alert.cweid);
|
|
4836
|
+
if (cwe !== void 0) req.cwe = cwe;
|
|
4615
4837
|
const controlType = deriveControlTypeFromTags(nistTags);
|
|
4616
4838
|
if (controlType !== void 0) req.controlType = controlType;
|
|
4839
|
+
const code = buildRequirementCode$1(alert);
|
|
4840
|
+
if (code !== void 0) req.code = code;
|
|
4617
4841
|
requirements.push(req);
|
|
4618
4842
|
}
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4843
|
+
return requirements;
|
|
4844
|
+
}
|
|
4845
|
+
const ZAP_RFC1123_LIKE = /^[A-Za-z]{3}, \d{1,2} [A-Za-z]{3} \d{4} \d{2}:\d{2}:\d{2}$/;
|
|
4846
|
+
function parseZapTimestamp(s) {
|
|
4847
|
+
const trimmed = s.trim();
|
|
4848
|
+
if (ZAP_RFC1123_LIKE.test(trimmed)) {
|
|
4849
|
+
const d = /* @__PURE__ */ new Date(`${trimmed} GMT`);
|
|
4850
|
+
if (!isNaN(d.getTime())) return d;
|
|
4626
4851
|
}
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
const
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4852
|
+
return parseTimestamp(s) ?? void 0;
|
|
4853
|
+
}
|
|
4854
|
+
async function convertZapToHdf(input, converterVersion = "1.0.0") {
|
|
4855
|
+
validateInputSize(input, "zap");
|
|
4856
|
+
registerAllFingerprints();
|
|
4857
|
+
const detected = detectConverter(input);
|
|
4858
|
+
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
|
|
4859
|
+
const resultsChecksum = await inputChecksum(input);
|
|
4860
|
+
const zapData = parseJSON(input);
|
|
4861
|
+
const sites = Array.isArray(zapData.site) ? zapData.site : [];
|
|
4862
|
+
const multiSite = sites.length > 1;
|
|
4863
|
+
const summary = `ZAP Version ${zapData["@version"] ?? "unknown"}`;
|
|
4864
|
+
const baselines = [];
|
|
4637
4865
|
const components = [];
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4866
|
+
sites.forEach((site, i) => {
|
|
4867
|
+
const targetName = site["@host"] ?? "Unknown Host";
|
|
4868
|
+
const siteName = site["@name"] ?? "";
|
|
4869
|
+
const requirements = buildSiteRequirements(site);
|
|
4870
|
+
if (requirements.length === 0) {
|
|
4871
|
+
let target = siteName || targetName;
|
|
4872
|
+
if (!target || target === "Unknown Host") target = "the target site";
|
|
4873
|
+
requirements.push(buildNoFindingsRequirement("zap-no-findings", `OWASP ZAP scanned ${target} and reported zero findings.`, /* @__PURE__ */ new Date()));
|
|
4874
|
+
}
|
|
4875
|
+
const baselineTitle = site["@name"] || site["@host"] ? `OWASP ZAP Scan of ${site["@name"] ?? targetName}` : "OWASP ZAP Scan";
|
|
4876
|
+
const baseline = createMinimalBaseline(multiSite ? `OWASP ZAP Scan: ${siteLabel(site, i)}` : "OWASP ZAP Scan", requirements, {
|
|
4877
|
+
resultsChecksum,
|
|
4878
|
+
title: baselineTitle,
|
|
4879
|
+
summary
|
|
4880
|
+
});
|
|
4881
|
+
if (site["@host"]) baseline.labels = { component: site["@host"] };
|
|
4882
|
+
baselines.push(baseline);
|
|
4883
|
+
if (site["@name"]) components.push({
|
|
4884
|
+
name: targetName,
|
|
4885
|
+
type: TargetType.Application,
|
|
4886
|
+
url: site["@name"]
|
|
4887
|
+
});
|
|
4888
|
+
else if (targetName !== "Unknown Host") components.push({
|
|
4889
|
+
name: targetName,
|
|
4890
|
+
type: TargetType.Application
|
|
4891
|
+
});
|
|
4646
4892
|
});
|
|
4893
|
+
if (baselines.length === 0) {
|
|
4894
|
+
const requirements = [buildNoFindingsRequirement("zap-no-findings", "OWASP ZAP scanned the target site and reported zero findings.", /* @__PURE__ */ new Date())];
|
|
4895
|
+
baselines.push(createMinimalBaseline("OWASP ZAP Scan", requirements, {
|
|
4896
|
+
resultsChecksum,
|
|
4897
|
+
title: "OWASP ZAP Scan",
|
|
4898
|
+
summary
|
|
4899
|
+
}));
|
|
4900
|
+
}
|
|
4901
|
+
const tool = { name: "OWASP ZAP" };
|
|
4902
|
+
if (zapData["@version"]) tool.version = zapData["@version"];
|
|
4647
4903
|
const hdf = {
|
|
4648
|
-
baselines
|
|
4904
|
+
baselines,
|
|
4649
4905
|
components,
|
|
4650
4906
|
generator: {
|
|
4651
4907
|
name: "zap-to-hdf",
|
|
@@ -5263,23 +5519,18 @@ function stringSlice(v) {
|
|
|
5263
5519
|
if (Array.isArray(v)) return v.filter((e) => typeof e === "string");
|
|
5264
5520
|
return [];
|
|
5265
5521
|
}
|
|
5266
|
-
/**
|
|
5522
|
+
/**
|
|
5523
|
+
* Most-significant status across a requirement's results[] (lossless), using
|
|
5524
|
+
* the canonical worst-wins ordering from @mitre/hdf-utilities.
|
|
5525
|
+
*/
|
|
5267
5526
|
function worstOfResults(req) {
|
|
5268
5527
|
const results = asArr(req.results) ?? [];
|
|
5269
|
-
const
|
|
5270
|
-
"failed",
|
|
5271
|
-
"error",
|
|
5272
|
-
"passed",
|
|
5273
|
-
"notReviewed",
|
|
5274
|
-
"notApplicable"
|
|
5275
|
-
];
|
|
5276
|
-
const present = /* @__PURE__ */ new Set();
|
|
5528
|
+
const statuses = [];
|
|
5277
5529
|
for (const rRaw of results) {
|
|
5278
5530
|
const r = asMap(rRaw);
|
|
5279
|
-
if (r)
|
|
5531
|
+
if (r) statuses.push(getStr(r, "status"));
|
|
5280
5532
|
}
|
|
5281
|
-
|
|
5282
|
-
return "notReviewed";
|
|
5533
|
+
return worstStatus(statuses);
|
|
5283
5534
|
}
|
|
5284
5535
|
/**
|
|
5285
5536
|
* Whether an HDF Result_Status is a failing verdict. Only 'failed' fails;
|
|
@@ -5535,10 +5786,34 @@ function maxImpact(ratings) {
|
|
|
5535
5786
|
return max;
|
|
5536
5787
|
}
|
|
5537
5788
|
/**
|
|
5538
|
-
*
|
|
5789
|
+
* Assembles structured requirement.cvss[] entries from the CycloneDX ratings. A
|
|
5790
|
+
* rating contributes an entry only when it carries a CVSS method
|
|
5791
|
+
* (CVSSv2/v3/v31/v4) and at least a score or a vector — ratings that only state a
|
|
5792
|
+
* qualitative severity (method "other") carry no CVSS metrics and are left out,
|
|
5793
|
+
* their severity already reflected in the requirement impact.
|
|
5539
5794
|
*/
|
|
5540
|
-
function
|
|
5541
|
-
|
|
5795
|
+
function cvssVersionFromMethod(method, vector) {
|
|
5796
|
+
if (vector !== void 0 && vector.startsWith("CVSS:")) return cvssVersionFromVector(vector);
|
|
5797
|
+
switch (method) {
|
|
5798
|
+
case "CVSSv2": return cvssVersionFromString("2.0");
|
|
5799
|
+
case "CVSSv4": return cvssVersionFromString("4.0");
|
|
5800
|
+
default: return cvssVersionFromVector(vector);
|
|
5801
|
+
}
|
|
5802
|
+
}
|
|
5803
|
+
function buildCvssEntries$3(ratings) {
|
|
5804
|
+
const entries = [];
|
|
5805
|
+
for (const r of ratings) {
|
|
5806
|
+
const hasCvssMethod = r.method !== void 0 && CVSS_METHODS.has(r.method);
|
|
5807
|
+
const hasMetric = r.score !== void 0 && r.score !== null || r.vector !== void 0 && r.vector !== "";
|
|
5808
|
+
if (!hasCvssMethod || !hasMetric) continue;
|
|
5809
|
+
entries.push(buildCvss$2({
|
|
5810
|
+
version: cvssVersionFromMethod(r.method, r.vector),
|
|
5811
|
+
baseScore: r.score,
|
|
5812
|
+
baseVector: r.vector,
|
|
5813
|
+
source: r.source?.name
|
|
5814
|
+
}));
|
|
5815
|
+
}
|
|
5816
|
+
return entries;
|
|
5542
5817
|
}
|
|
5543
5818
|
/**
|
|
5544
5819
|
* Formats a component reference as a code_desc string.
|
|
@@ -5606,8 +5881,8 @@ async function convertCyclonedxToHdf(input, converterVersion = "1.0.0") {
|
|
|
5606
5881
|
nist,
|
|
5607
5882
|
cci: nistToCci(nist)
|
|
5608
5883
|
};
|
|
5609
|
-
|
|
5610
|
-
|
|
5884
|
+
const cweIds = cwes.map((c) => `CWE-${c}`);
|
|
5885
|
+
const cvssEntries = buildCvssEntries$3(ratings);
|
|
5611
5886
|
const descriptions = [];
|
|
5612
5887
|
const defaultParts = [];
|
|
5613
5888
|
if (vuln.description) defaultParts.push(`Description: ${vuln.description}`);
|
|
@@ -5635,6 +5910,8 @@ async function convertCyclonedxToHdf(input, converterVersion = "1.0.0") {
|
|
|
5635
5910
|
const results = affects.length > 0 ? affects.map((affect) => toResult(formatCodeDesc$4(componentLookup, affect.ref))) : [toResult(`Vulnerability ${vuln.id}`)];
|
|
5636
5911
|
const title = vuln.source?.name ? `${vuln.id} (${vuln.source.name})` : vuln.id;
|
|
5637
5912
|
const req = createRequirement(vuln.id, title, descriptions, impact, results, { tags });
|
|
5913
|
+
if (cvssEntries.length > 0) req.cvss = cvssEntries;
|
|
5914
|
+
if (cweIds.length > 0) req.cwe = cweIds;
|
|
5638
5915
|
const controlType = deriveControlTypeFromTags(nist);
|
|
5639
5916
|
if (controlType !== void 0) req.controlType = controlType;
|
|
5640
5917
|
requirements.push(req);
|
|
@@ -5658,7 +5935,6 @@ async function convertCyclonedxToHdf(input, converterVersion = "1.0.0") {
|
|
|
5658
5935
|
generatorName: "cyclonedx-to-hdf",
|
|
5659
5936
|
converterVersion,
|
|
5660
5937
|
toolName: "CycloneDX",
|
|
5661
|
-
toolFormat: "JSON",
|
|
5662
5938
|
baselines: [baseline],
|
|
5663
5939
|
components: [{
|
|
5664
5940
|
...targetComponent,
|
|
@@ -6769,7 +7045,7 @@ function collectIdentifierTags(identifiers) {
|
|
|
6769
7045
|
}
|
|
6770
7046
|
return result;
|
|
6771
7047
|
}
|
|
6772
|
-
function buildCodeDesc$
|
|
7048
|
+
function buildCodeDesc$5(scanType, location) {
|
|
6773
7049
|
if (!location) return "";
|
|
6774
7050
|
switch (scanType) {
|
|
6775
7051
|
case "sast":
|
|
@@ -6847,7 +7123,7 @@ async function convertGitlabToHdf(input, converterVersion = "1.0.0") {
|
|
|
6847
7123
|
});
|
|
6848
7124
|
const result = {
|
|
6849
7125
|
status: ResultStatus.Failed,
|
|
6850
|
-
codeDesc: buildCodeDesc$
|
|
7126
|
+
codeDesc: buildCodeDesc$5(scanType, vuln.location),
|
|
6851
7127
|
startTime: startTime ? parseTimestamp(startTime) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z") : /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
|
|
6852
7128
|
};
|
|
6853
7129
|
const impact = gitlabSeverityToImpact(vuln.severity ?? "Unknown");
|
|
@@ -6855,6 +7131,7 @@ async function convertGitlabToHdf(input, converterVersion = "1.0.0") {
|
|
|
6855
7131
|
id: vuln.id,
|
|
6856
7132
|
title: vuln.name ?? vuln.id,
|
|
6857
7133
|
impact,
|
|
7134
|
+
code: JSON.stringify(vuln, null, 2),
|
|
6858
7135
|
results: [result],
|
|
6859
7136
|
tags,
|
|
6860
7137
|
descriptions,
|
|
@@ -6874,10 +7151,7 @@ async function convertGitlabToHdf(input, converterVersion = "1.0.0") {
|
|
|
6874
7151
|
title: `GitLab ${label} Security Scan`,
|
|
6875
7152
|
summary: `Scanner: ${scannerName}${scannerVersion ? ` v${scannerVersion}` : ""}`
|
|
6876
7153
|
});
|
|
6877
|
-
const tool = {
|
|
6878
|
-
name: scannerName,
|
|
6879
|
-
format: "JSON"
|
|
6880
|
-
};
|
|
7154
|
+
const tool = { name: scannerName };
|
|
6881
7155
|
if (scannerVersion) tool.version = scannerVersion;
|
|
6882
7156
|
const components = [];
|
|
6883
7157
|
const targetType = scanTypeToTargetType(scanType);
|
|
@@ -6910,6 +7184,20 @@ const TRUFFLEHOG_CCI = [
|
|
|
6910
7184
|
"CCI-002367"
|
|
6911
7185
|
];
|
|
6912
7186
|
/**
|
|
7187
|
+
* A verified secret is a confirmed-live credential (TruffleHog reached the
|
|
7188
|
+
* provider and the credential authenticated) and rates high; an unverified
|
|
7189
|
+
* candidate rates medium.
|
|
7190
|
+
*/
|
|
7191
|
+
const IMPACT_VERIFIED = .7;
|
|
7192
|
+
const IMPACT_UNVERIFIED = .5;
|
|
7193
|
+
/**
|
|
7194
|
+
* Rate a requirement by its strongest signal: any verified finding in the group
|
|
7195
|
+
* elevates the whole requirement to the verified impact.
|
|
7196
|
+
*/
|
|
7197
|
+
function groupImpact(findings) {
|
|
7198
|
+
return findings.some((f) => f.Verified) ? IMPACT_VERIFIED : IMPACT_UNVERIFIED;
|
|
7199
|
+
}
|
|
7200
|
+
/**
|
|
6913
7201
|
* Parse TruffleHog input as JSON array, single object, or NDJSON.
|
|
6914
7202
|
*/
|
|
6915
7203
|
function parseFindings(input) {
|
|
@@ -6969,7 +7257,7 @@ function buildMessage(f) {
|
|
|
6969
7257
|
/**
|
|
6970
7258
|
* Build the Result.CodeDesc as JSON of SourceMetadata.
|
|
6971
7259
|
*/
|
|
6972
|
-
function buildCodeDesc$
|
|
7260
|
+
function buildCodeDesc$4(f) {
|
|
6973
7261
|
return JSON.stringify(f.SourceMetadata ?? {});
|
|
6974
7262
|
}
|
|
6975
7263
|
/**
|
|
@@ -7012,12 +7300,12 @@ function buildRequirement$12(reqID, findings) {
|
|
|
7012
7300
|
data: rep.DetectorDescription ?? `${rep.DetectorName} secret detected by ${rep.DecoderName} decoder`
|
|
7013
7301
|
}];
|
|
7014
7302
|
const results = findings.map((f) => createResult(ResultStatus.Failed, buildMessage(f), {
|
|
7015
|
-
codeDesc: buildCodeDesc$
|
|
7303
|
+
codeDesc: buildCodeDesc$4(f),
|
|
7016
7304
|
startTime: getTimestamp(f)
|
|
7017
7305
|
}));
|
|
7018
7306
|
const sourceFile = getSourceFile(rep);
|
|
7019
7307
|
const sourceLine = getSourceLine(rep);
|
|
7020
|
-
const req = createRequirement(reqID, title, descriptions,
|
|
7308
|
+
const req = createRequirement(reqID, title, descriptions, groupImpact(findings), results, {
|
|
7021
7309
|
tags,
|
|
7022
7310
|
sourceLocation: sourceFile ? {
|
|
7023
7311
|
ref: sourceFile,
|
|
@@ -7057,10 +7345,7 @@ async function convertTrufflehogToHdf(input, converterVersion = "1.0.0") {
|
|
|
7057
7345
|
name: "trufflehog-to-hdf",
|
|
7058
7346
|
version: converterVersion
|
|
7059
7347
|
},
|
|
7060
|
-
tool: {
|
|
7061
|
-
name: "TruffleHog",
|
|
7062
|
-
format: "JSON"
|
|
7063
|
-
},
|
|
7348
|
+
tool: { name: "TruffleHog" },
|
|
7064
7349
|
timestamp: /* @__PURE__ */ new Date()
|
|
7065
7350
|
};
|
|
7066
7351
|
const repoURL = findGitRepoURL(limitedFindings);
|
|
@@ -7194,7 +7479,6 @@ async function convertHipcheckToHdf(input, converterVersion = "1.0.0") {
|
|
|
7194
7479
|
generatorName: "hipcheck-to-hdf",
|
|
7195
7480
|
converterVersion,
|
|
7196
7481
|
toolName: "Hipcheck",
|
|
7197
|
-
toolFormat: "JSON",
|
|
7198
7482
|
toolVersion: report.hipcheck_version,
|
|
7199
7483
|
baselines: [baseline],
|
|
7200
7484
|
components: ident ? [{
|
|
@@ -7267,10 +7551,7 @@ async function convertBurpsuiteToHdf(input, converterVersion = "1.0.0") {
|
|
|
7267
7551
|
resultsChecksum,
|
|
7268
7552
|
title: `BurpSuite Scan: ${targetName}`
|
|
7269
7553
|
});
|
|
7270
|
-
const tool = {
|
|
7271
|
-
name: "BurpSuite",
|
|
7272
|
-
format: "XML"
|
|
7273
|
-
};
|
|
7554
|
+
const tool = { name: "BurpSuite" };
|
|
7274
7555
|
if (burpVersion) tool.version = burpVersion;
|
|
7275
7556
|
const hdf = {
|
|
7276
7557
|
baselines: [baseline],
|
|
@@ -7439,6 +7720,17 @@ function parseDate(dateStr) {
|
|
|
7439
7720
|
return parseTimestamp(month ? `${month[3]}-${MONTH_ABBR[month[1]] ?? "00"}-${month[2].padStart(2, "0")} ${month[4]}` : trimmed) ?? ZERO;
|
|
7440
7721
|
}
|
|
7441
7722
|
/**
|
|
7723
|
+
* Renders a finding's parsed row (column→value map) as the indented JSON blob
|
|
7724
|
+
* carried in requirement.code. DBProtect ships no literal check source, so the
|
|
7725
|
+
* row itself is the richest available representation. Keys are sorted so the
|
|
7726
|
+
* bytes match the Go twin, whose encoding/json emits map keys in sorted order.
|
|
7727
|
+
*/
|
|
7728
|
+
function marshalFindingCode(f) {
|
|
7729
|
+
const sorted = {};
|
|
7730
|
+
for (const key of Object.keys(f).sort()) sorted[key] = f[key];
|
|
7731
|
+
return JSON.stringify(sorted, null, 2);
|
|
7732
|
+
}
|
|
7733
|
+
/**
|
|
7442
7734
|
* Builds a single EvaluatedRequirement from a group of findings sharing a Check ID.
|
|
7443
7735
|
*/
|
|
7444
7736
|
function buildRequirement$10(checkID, findings, hasStatus) {
|
|
@@ -7457,6 +7749,7 @@ function buildRequirement$10(checkID, findings, hasStatus) {
|
|
|
7457
7749
|
});
|
|
7458
7750
|
const req = createRequirement(checkID, rep["Check"] ?? "", descriptions, getImpact$6(rep["Risk DV"] ?? ""), results, { tags });
|
|
7459
7751
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
7752
|
+
req.code = marshalFindingCode(rep);
|
|
7460
7753
|
const controlType = deriveControlTypeFromTags([...nist]);
|
|
7461
7754
|
if (controlType !== void 0) req.controlType = controlType;
|
|
7462
7755
|
return req;
|
|
@@ -7508,7 +7801,6 @@ async function convertDbprotectToHdf(input, converterVersion = "1.0.0") {
|
|
|
7508
7801
|
generatorName: "dbprotect-to-hdf",
|
|
7509
7802
|
converterVersion,
|
|
7510
7803
|
toolName: "DBProtect",
|
|
7511
|
-
toolFormat: "XML",
|
|
7512
7804
|
baselines: [baseline],
|
|
7513
7805
|
components: [{
|
|
7514
7806
|
name: targetName,
|
|
@@ -7552,30 +7844,6 @@ function buildSummary(result) {
|
|
|
7552
7844
|
return `Package Vulnerability Summary: ${result.vulnerabilityDistribution ? String(result.vulnerabilityDistribution.total) : "N/A"} Application Compliance Issue Total: ${result.complianceDistribution ? String(result.complianceDistribution.total) : "N/A"}`;
|
|
7553
7845
|
}
|
|
7554
7846
|
/**
|
|
7555
|
-
* Detects CVSS schema version enum from the vector prefix. Defaults to 3.1
|
|
7556
|
-
* since modern Twistlock exclusively emits 3.x output.
|
|
7557
|
-
*/
|
|
7558
|
-
function cvssVersionFromVector(vector) {
|
|
7559
|
-
if (!vector) return Version.The31;
|
|
7560
|
-
if (vector.startsWith("CVSS:2.0/")) return Version.The20;
|
|
7561
|
-
if (vector.startsWith("CVSS:3.0/")) return Version.The30;
|
|
7562
|
-
if (vector.startsWith("CVSS:4.0/")) return Version.The40;
|
|
7563
|
-
return Version.The31;
|
|
7564
|
-
}
|
|
7565
|
-
/**
|
|
7566
|
-
* Maps cvssScoreToSeverity('low'|'medium'|...) into the CVSSSeverity enum.
|
|
7567
|
-
*/
|
|
7568
|
-
function cvssSeverityFromScore(score) {
|
|
7569
|
-
switch (cvssScoreToSeverity(score)) {
|
|
7570
|
-
case "none": return CVSSSeverity.None;
|
|
7571
|
-
case "low": return CVSSSeverity.Low;
|
|
7572
|
-
case "medium": return CVSSSeverity.Medium;
|
|
7573
|
-
case "high": return CVSSSeverity.High;
|
|
7574
|
-
case "critical": return CVSSSeverity.Critical;
|
|
7575
|
-
default: return;
|
|
7576
|
-
}
|
|
7577
|
-
}
|
|
7578
|
-
/**
|
|
7579
7847
|
* Builds a Cvss entry from a Twistlock vulnerability. Returns undefined only
|
|
7580
7848
|
* when neither a score nor a vector is available. When the vendor emits a
|
|
7581
7849
|
* score but no vector (common in Twistlock/Prisma Cloud output), the Cvss
|
|
@@ -7586,16 +7854,12 @@ function buildCvss(vuln) {
|
|
|
7586
7854
|
const hasScore = typeof vuln.cvss === "number" && Number.isFinite(vuln.cvss);
|
|
7587
7855
|
const hasVector = !!vuln.vector;
|
|
7588
7856
|
if (!hasScore && !hasVector) return void 0;
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
}
|
|
7595
|
-
if (hasVector) cv.baseVector = vuln.vector;
|
|
7596
|
-
const source = vuln.cve ?? vuln.id;
|
|
7597
|
-
if (source) cv.source = source;
|
|
7598
|
-
return cv;
|
|
7857
|
+
return buildCvss$2({
|
|
7858
|
+
version: cvssVersionFromVector(vuln.vector),
|
|
7859
|
+
baseScore: hasScore ? vuln.cvss : void 0,
|
|
7860
|
+
baseVector: vuln.vector,
|
|
7861
|
+
source: vuln.cve ?? vuln.id
|
|
7862
|
+
});
|
|
7599
7863
|
}
|
|
7600
7864
|
const CWE_REGEX = /cwe[-_]?(\d+)/gi;
|
|
7601
7865
|
/**
|
|
@@ -7785,7 +8049,6 @@ async function convertTwistlockToHdf(input, converterVersion = "1.0.0") {
|
|
|
7785
8049
|
generatorName: "twistlock-to-hdf",
|
|
7786
8050
|
converterVersion,
|
|
7787
8051
|
toolName: "Twistlock",
|
|
7788
|
-
toolFormat: "JSON",
|
|
7789
8052
|
baselines,
|
|
7790
8053
|
components: [component],
|
|
7791
8054
|
timestamp: /* @__PURE__ */ new Date()
|
|
@@ -7826,16 +8089,32 @@ function getCweIDs(cwes) {
|
|
|
7826
8089
|
return cwes.map((cwe) => `CWE-${cwe.cweId}`);
|
|
7827
8090
|
}
|
|
7828
8091
|
/**
|
|
8092
|
+
* Collects CVE identifiers from vulnerability.aliases[].cveId, deduped in
|
|
8093
|
+
* first-seen order. The finding.matrix (requirement id) is a UUID composite, so
|
|
8094
|
+
* the CVE has no other home; it goes to tags.cve (interim, pending an
|
|
8095
|
+
* identifiers[] schema field).
|
|
8096
|
+
*/
|
|
8097
|
+
function getCVEs(vuln) {
|
|
8098
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8099
|
+
const out = [];
|
|
8100
|
+
for (const alias of vuln.aliases ?? []) if (alias.cveId && !seen.has(alias.cveId)) {
|
|
8101
|
+
seen.add(alias.cveId);
|
|
8102
|
+
out.push(alias.cveId);
|
|
8103
|
+
}
|
|
8104
|
+
return out;
|
|
8105
|
+
}
|
|
8106
|
+
/**
|
|
7829
8107
|
* Builds a single EvaluatedRequirement from a Dependency-Track finding.
|
|
7830
8108
|
*/
|
|
7831
8109
|
function buildRequirement$8(finding, timestamp) {
|
|
7832
8110
|
const cweIDs = getCweIDs(finding.vulnerability.cwes);
|
|
8111
|
+
const cveIDs = getCVEs(finding.vulnerability);
|
|
7833
8112
|
const nist = mapCWEToNIST(cweIDs, DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
|
|
7834
8113
|
const tags = {
|
|
7835
8114
|
nist,
|
|
7836
8115
|
cci: nistToCci(nist)
|
|
7837
8116
|
};
|
|
7838
|
-
if (
|
|
8117
|
+
if (cveIDs.length > 0) tags["cve"] = cveIDs;
|
|
7839
8118
|
const descriptions = [{
|
|
7840
8119
|
label: "default",
|
|
7841
8120
|
data: finding.vulnerability.description ?? ""
|
|
@@ -7855,8 +8134,10 @@ function buildRequirement$8(finding, timestamp) {
|
|
|
7855
8134
|
})];
|
|
7856
8135
|
const req = createRequirement(finding.matrix, getTitle$1(finding), descriptions, getImpact$5(finding.vulnerability.severity), results, { tags });
|
|
7857
8136
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
8137
|
+
req.code = JSON.stringify(finding, null, 2);
|
|
7858
8138
|
const controlType = deriveControlTypeFromTags(nist);
|
|
7859
8139
|
if (controlType !== void 0) req.controlType = controlType;
|
|
8140
|
+
if (cweIDs.length > 0) req.cwe = cweIDs;
|
|
7860
8141
|
const pkg = buildAffectedPackageFromComponent(finding.component);
|
|
7861
8142
|
if (pkg) req.affectedPackages = [pkg];
|
|
7862
8143
|
return req;
|
|
@@ -7908,7 +8189,7 @@ async function convertDeptrackToHdf(input, converterVersion = "1.0.0") {
|
|
|
7908
8189
|
generatorName: "deptrack-to-hdf",
|
|
7909
8190
|
converterVersion,
|
|
7910
8191
|
toolName: "Dependency-Track",
|
|
7911
|
-
toolFormat: "
|
|
8192
|
+
toolFormat: "FPF",
|
|
7912
8193
|
baselines: [baseline],
|
|
7913
8194
|
components: [{
|
|
7914
8195
|
name: targetName,
|
|
@@ -7927,12 +8208,79 @@ async function hashID(summary) {
|
|
|
7927
8208
|
return (await sha256(summary)).substring(0, 32);
|
|
7928
8209
|
}
|
|
7929
8210
|
/**
|
|
7930
|
-
*
|
|
8211
|
+
* Collect the distinct CWE identifiers across every CVE entry, preserving
|
|
8212
|
+
* first-seen order. These feed both requirement.cwe[] and the CWE→NIST mapping.
|
|
7931
8213
|
*/
|
|
7932
8214
|
function extractCWEs$1(entry) {
|
|
7933
|
-
const
|
|
7934
|
-
|
|
7935
|
-
|
|
8215
|
+
const out = [];
|
|
8216
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8217
|
+
for (const cve of entry.component_versions?.more_details?.cves ?? []) for (const cwe of cve.cwe ?? []) if (cwe && !seen.has(cwe)) {
|
|
8218
|
+
seen.add(cwe);
|
|
8219
|
+
out.push(cwe);
|
|
8220
|
+
}
|
|
8221
|
+
return out;
|
|
8222
|
+
}
|
|
8223
|
+
/**
|
|
8224
|
+
* Collect the distinct CVE identifiers across every CVE entry, preserving
|
|
8225
|
+
* first-seen order. The requirement id is a summary hash (never the CVE), so
|
|
8226
|
+
* tags.cve is where the CVE list lives.
|
|
8227
|
+
*/
|
|
8228
|
+
function extractCVEs$1(entry) {
|
|
8229
|
+
const out = [];
|
|
8230
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8231
|
+
for (const cve of entry.component_versions?.more_details?.cves ?? []) if (cve.cve && !seen.has(cve.cve)) {
|
|
8232
|
+
seen.add(cve.cve);
|
|
8233
|
+
out.push(cve.cve);
|
|
8234
|
+
}
|
|
8235
|
+
return out;
|
|
8236
|
+
}
|
|
8237
|
+
/**
|
|
8238
|
+
* Split a JFrog cvss_v2/cvss_v3 field ("<score>/<vector>", e.g.
|
|
8239
|
+
* "9.8/CVSS:3.1/AV:N/...") on the FIRST "/" into the numeric base score and the
|
|
8240
|
+
* remaining vector. A field with no "/" is a bare score with no vector; an
|
|
8241
|
+
* unparseable score yields undefined (the vector may still stand alone).
|
|
8242
|
+
*/
|
|
8243
|
+
function parseCvssField(field) {
|
|
8244
|
+
if (!field) return { vector: "" };
|
|
8245
|
+
const idx = field.indexOf("/");
|
|
8246
|
+
let scorePart = field;
|
|
8247
|
+
let vector = "";
|
|
8248
|
+
if (idx >= 0) {
|
|
8249
|
+
scorePart = field.slice(0, idx);
|
|
8250
|
+
vector = field.slice(idx + 1);
|
|
8251
|
+
}
|
|
8252
|
+
const parsed = Number.parseFloat(scorePart.trim());
|
|
8253
|
+
return {
|
|
8254
|
+
score: Number.isFinite(parsed) ? parsed : void 0,
|
|
8255
|
+
vector
|
|
8256
|
+
};
|
|
8257
|
+
}
|
|
8258
|
+
/**
|
|
8259
|
+
* Assemble the structured requirement.cvss[] for a group's representative
|
|
8260
|
+
* entry. Each CVE contributes its v3 metric (version from the vector prefix)
|
|
8261
|
+
* then its v2 metric (always CVSS 2.0 — the cvss_v2 vector prefix is
|
|
8262
|
+
* inconsistent, so the field name is the authority). A field with neither a
|
|
8263
|
+
* score nor a vector is skipped.
|
|
8264
|
+
*/
|
|
8265
|
+
function buildCvssEntries$2(cves) {
|
|
8266
|
+
const out = [];
|
|
8267
|
+
for (const cve of cves ?? []) {
|
|
8268
|
+
const v3 = parseCvssField(cve.cvss_v3);
|
|
8269
|
+
if (v3.score !== void 0 || v3.vector !== "") out.push(buildCvss$2({
|
|
8270
|
+
version: cvssVersionFromVector(v3.vector),
|
|
8271
|
+
baseScore: v3.score,
|
|
8272
|
+
baseVector: v3.vector,
|
|
8273
|
+
source: cve.cve
|
|
8274
|
+
}));
|
|
8275
|
+
const v2 = parseCvssField(cve.cvss_v2);
|
|
8276
|
+
if (v2.score !== void 0 || v2.vector !== "") out.push(buildCvss$2({
|
|
8277
|
+
version: cvssVersionFromString("2.0"),
|
|
8278
|
+
baseScore: v2.score,
|
|
8279
|
+
baseVector: v2.vector,
|
|
8280
|
+
source: cve.cve
|
|
8281
|
+
}));
|
|
8282
|
+
}
|
|
8283
|
+
return out;
|
|
7936
8284
|
}
|
|
7937
8285
|
/**
|
|
7938
8286
|
* Build description from more_details, including CVE data.
|
|
@@ -7970,17 +8318,57 @@ function formatCodeDesc$1(entry) {
|
|
|
7970
8318
|
return parts.join("\n").replace(/,/g, ", ");
|
|
7971
8319
|
}
|
|
7972
8320
|
/**
|
|
8321
|
+
* Render an Xray entry as the indented JSON blob carried in the requirement's
|
|
8322
|
+
* code field (the ionchannel pattern), so Heimdall's CODE tab shows the raw
|
|
8323
|
+
* finding. The object is reconstructed with a fixed key order and full field set
|
|
8324
|
+
* matching the Go twin's struct marshal, keeping Go and TS byte-identical
|
|
8325
|
+
* (verified by the shared snapshot test).
|
|
8326
|
+
*/
|
|
8327
|
+
function marshalEntryCode(entry) {
|
|
8328
|
+
const cv = entry.component_versions;
|
|
8329
|
+
const md = cv?.more_details;
|
|
8330
|
+
const cves = md?.cves;
|
|
8331
|
+
const codeObject = {
|
|
8332
|
+
id: entry.id ?? "",
|
|
8333
|
+
severity: entry.severity ?? "",
|
|
8334
|
+
summary: entry.summary ?? "",
|
|
8335
|
+
issue_type: entry.issue_type ?? "",
|
|
8336
|
+
provider: entry.provider ?? "",
|
|
8337
|
+
component: entry.component ?? "",
|
|
8338
|
+
source_id: entry.source_id ?? "",
|
|
8339
|
+
source_comp_id: entry.source_comp_id ?? "",
|
|
8340
|
+
component_versions: {
|
|
8341
|
+
id: cv?.id ?? "",
|
|
8342
|
+
vulnerable_versions: cv?.vulnerable_versions ?? null,
|
|
8343
|
+
fixed_versions: cv?.fixed_versions ?? null,
|
|
8344
|
+
more_details: {
|
|
8345
|
+
cves: cves ? cves.map((c) => ({
|
|
8346
|
+
cve: c.cve ?? "",
|
|
8347
|
+
cwe: c.cwe ?? null,
|
|
8348
|
+
cvss_v2: c.cvss_v2 ?? "",
|
|
8349
|
+
cvss_v3: c.cvss_v3 ?? ""
|
|
8350
|
+
})) : null,
|
|
8351
|
+
description: md?.description ?? "",
|
|
8352
|
+
provider: md?.provider ?? ""
|
|
8353
|
+
}
|
|
8354
|
+
},
|
|
8355
|
+
edited: entry.edited ?? ""
|
|
8356
|
+
};
|
|
8357
|
+
return JSON.stringify(codeObject, null, 2);
|
|
8358
|
+
}
|
|
8359
|
+
/**
|
|
7973
8360
|
* Builds a single EvaluatedRequirement from a group of entries sharing an ID.
|
|
7974
8361
|
*/
|
|
7975
8362
|
function buildRequirement$7(entryID, entries, scanTime) {
|
|
7976
8363
|
const rep = entries[0];
|
|
7977
8364
|
const cweIDs = extractCWEs$1(rep);
|
|
8365
|
+
const cveIDs = extractCVEs$1(rep);
|
|
7978
8366
|
const nist = mapCWEToNIST(cweIDs, DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
|
|
7979
8367
|
const tags = {
|
|
7980
8368
|
nist,
|
|
7981
8369
|
cci: nistToCci(nist)
|
|
7982
8370
|
};
|
|
7983
|
-
if (
|
|
8371
|
+
if (cveIDs.length > 0) tags["cve"] = cveIDs;
|
|
7984
8372
|
const descriptions = [{
|
|
7985
8373
|
label: "default",
|
|
7986
8374
|
data: formatDescription(rep)
|
|
@@ -7991,9 +8379,13 @@ function buildRequirement$7(entryID, entries, scanTime) {
|
|
|
7991
8379
|
startTime: scanTime
|
|
7992
8380
|
}));
|
|
7993
8381
|
const req = createRequirement(entryID, rep.summary, descriptions, severityToImpact$1(rep.severity), results, { tags });
|
|
8382
|
+
req.code = marshalEntryCode(rep);
|
|
7994
8383
|
const controlType = deriveControlTypeFromTags(nist);
|
|
7995
8384
|
if (controlType !== void 0) req.controlType = controlType;
|
|
7996
8385
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
8386
|
+
if (cweIDs.length > 0) req.cwe = cweIDs;
|
|
8387
|
+
const cvss = buildCvssEntries$2(rep.component_versions?.more_details?.cves);
|
|
8388
|
+
if (cvss.length > 0) req.cvss = cvss;
|
|
7997
8389
|
const pkg = buildAffectedPackageFromEntry(rep);
|
|
7998
8390
|
if (pkg) req.affectedPackages = [pkg];
|
|
7999
8391
|
return req;
|
|
@@ -8072,7 +8464,6 @@ async function convertJfrogXrayToHdf(input, converterVersion = "1.0.0") {
|
|
|
8072
8464
|
generatorName: "jfrog-xray-to-hdf",
|
|
8073
8465
|
converterVersion,
|
|
8074
8466
|
toolName: "JFrog Xray",
|
|
8075
|
-
toolFormat: "JSON",
|
|
8076
8467
|
baselines: [createMinimalBaseline("JFrog Xray Scan", requirements, { resultsChecksum })],
|
|
8077
8468
|
components: [{
|
|
8078
8469
|
name: "JFrog Xray Scan",
|
|
@@ -8091,6 +8482,43 @@ function extractCWEs(description) {
|
|
|
8091
8482
|
return extractCWEIDs(description).map((id) => `CWE-${id}`);
|
|
8092
8483
|
}
|
|
8093
8484
|
/**
|
|
8485
|
+
* Collects the distinct CVE identifiers a vulnerability carries in its cves[]
|
|
8486
|
+
* field, preserving first-seen order. The requirement ID is a
|
|
8487
|
+
* name/package/version composite (not the CVE), so tags.cve is where the CVE
|
|
8488
|
+
* list lives.
|
|
8489
|
+
*/
|
|
8490
|
+
function extractCVEs(vuln) {
|
|
8491
|
+
const out = [];
|
|
8492
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8493
|
+
for (const cve of vuln.cves ?? []) if (cve && !seen.has(cve)) {
|
|
8494
|
+
seen.add(cve);
|
|
8495
|
+
out.push(cve);
|
|
8496
|
+
}
|
|
8497
|
+
return out;
|
|
8498
|
+
}
|
|
8499
|
+
/**
|
|
8500
|
+
* Assembles the structured requirement.cvss[] from the scoring a vulnerability
|
|
8501
|
+
* carries. NeuVector emits a CVSS v3 vector (version-prefixed) and, separately,
|
|
8502
|
+
* a legacy v2 vector (prefix-less). The v3 metric is preferred; the v2 metric
|
|
8503
|
+
* is the fallback. A vulnerability with neither vector contributes no entry —
|
|
8504
|
+
* its score still drives impact.
|
|
8505
|
+
*/
|
|
8506
|
+
function buildCvssEntries$1(vuln) {
|
|
8507
|
+
if (vuln.vectors_v3) return [buildCvss$2({
|
|
8508
|
+
version: cvssVersionFromVector(vuln.vectors_v3),
|
|
8509
|
+
baseScore: vuln.score_v3 > 0 ? vuln.score_v3 : void 0,
|
|
8510
|
+
baseVector: vuln.vectors_v3,
|
|
8511
|
+
source: "NeuVector"
|
|
8512
|
+
})];
|
|
8513
|
+
if (vuln.vectors) return [buildCvss$2({
|
|
8514
|
+
version: cvssVersionFromString("2.0"),
|
|
8515
|
+
baseScore: vuln.score > 0 ? vuln.score : void 0,
|
|
8516
|
+
baseVector: vuln.vectors,
|
|
8517
|
+
source: "NeuVector"
|
|
8518
|
+
})];
|
|
8519
|
+
return [];
|
|
8520
|
+
}
|
|
8521
|
+
/**
|
|
8094
8522
|
* Computes the HDF impact from NeuVector CVSS scores.
|
|
8095
8523
|
* Prefers CVSS v3 score; falls back to CVSS v2 if v3 is 0.
|
|
8096
8524
|
* Impact is normalized to 0.0-1.0 by dividing by 10.
|
|
@@ -8119,29 +8547,66 @@ function vulnMessage(vuln) {
|
|
|
8119
8547
|
if (!vuln.fixed_version) return `Vulnerable package ${vuln.package_name} is at version ${vuln.package_version}. No fixed version available.`;
|
|
8120
8548
|
return `Vulnerable package ${vuln.package_name} is at version ${vuln.package_version}. Update to fixed version ${vuln.fixed_version}.`;
|
|
8121
8549
|
}
|
|
8550
|
+
const WHITESPACE_RUN = /[ \t\n\r]+/g;
|
|
8551
|
+
const CODE_DESC_SNIPPET_RUNES = 100;
|
|
8552
|
+
/**
|
|
8553
|
+
* Returns the CVSS score used in code_desc: CVSS v3 preferred, else v2.
|
|
8554
|
+
*/
|
|
8555
|
+
function cvssScore(vuln) {
|
|
8556
|
+
return vuln.score_v3 > 0 ? vuln.score_v3 : vuln.score;
|
|
8557
|
+
}
|
|
8558
|
+
/**
|
|
8559
|
+
* Collapses the description to a single line and truncates it to
|
|
8560
|
+
* CODE_DESC_SNIPPET_RUNES code points (Array.from matches Go's []rune).
|
|
8561
|
+
*/
|
|
8562
|
+
function descSnippet(description) {
|
|
8563
|
+
const collapsed = description.replace(WHITESPACE_RUN, " ").trim();
|
|
8564
|
+
const runes = Array.from(collapsed);
|
|
8565
|
+
return runes.length > CODE_DESC_SNIPPET_RUNES ? runes.slice(0, CODE_DESC_SNIPPET_RUNES).join("") + "…" : collapsed;
|
|
8566
|
+
}
|
|
8567
|
+
/**
|
|
8568
|
+
* Builds the pipe-joined result code_desc from the fields the vuln carries:
|
|
8569
|
+
* package@version | name | CVSS score | description snippet. Only parts the
|
|
8570
|
+
* source actually provides are included.
|
|
8571
|
+
*/
|
|
8572
|
+
function buildCodeDesc$3(vuln) {
|
|
8573
|
+
const parts = [];
|
|
8574
|
+
if (vuln.package_name) parts.push(vuln.package_version ? `${vuln.package_name}@${vuln.package_version}` : vuln.package_name);
|
|
8575
|
+
if (vuln.name) parts.push(vuln.name);
|
|
8576
|
+
const score = cvssScore(vuln);
|
|
8577
|
+
if (score > 0) parts.push(`CVSS ${score}`);
|
|
8578
|
+
const snippet = descSnippet(vuln.description);
|
|
8579
|
+
if (snippet) parts.push(snippet);
|
|
8580
|
+
return parts.join(" | ");
|
|
8581
|
+
}
|
|
8122
8582
|
/**
|
|
8123
8583
|
* Builds a single EvaluatedRequirement from a NeuVector vulnerability.
|
|
8124
8584
|
*/
|
|
8125
8585
|
function buildRequirement$6(vuln, scanTime) {
|
|
8126
8586
|
const cweIDs = extractCWEs(vuln.description);
|
|
8587
|
+
const cveIDs = extractCVEs(vuln);
|
|
8127
8588
|
const nist = mapCWEToNIST(cweIDs, DEFAULT_REMEDIATION_NIST_TAGS);
|
|
8128
8589
|
const tags = {
|
|
8129
8590
|
nist,
|
|
8130
8591
|
cci: nistToCci(nist)
|
|
8131
8592
|
};
|
|
8132
|
-
if (
|
|
8593
|
+
if (cveIDs.length > 0) tags["cve"] = cveIDs;
|
|
8133
8594
|
const descriptions = [{
|
|
8134
8595
|
label: "default",
|
|
8135
8596
|
data: vuln.description
|
|
8136
8597
|
}];
|
|
8137
8598
|
const results = [createResult(ResultStatus.Failed, vulnMessage(vuln), {
|
|
8138
|
-
codeDesc:
|
|
8599
|
+
codeDesc: buildCodeDesc$3(vuln),
|
|
8139
8600
|
startTime: scanTime
|
|
8140
8601
|
})];
|
|
8141
8602
|
const req = createRequirement(vulnID(vuln), vulnTitle(vuln), descriptions, getImpact$4(vuln), results, { tags });
|
|
8603
|
+
req.code = JSON.stringify(vuln, null, 2);
|
|
8142
8604
|
const controlType = deriveControlTypeFromTags(nist);
|
|
8143
8605
|
if (controlType !== void 0) req.controlType = controlType;
|
|
8144
8606
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
8607
|
+
if (cweIDs.length > 0) req.cwe = cweIDs;
|
|
8608
|
+
const cvss = buildCvssEntries$1(vuln);
|
|
8609
|
+
if (cvss.length > 0) req.cvss = cvss;
|
|
8145
8610
|
const cpe23 = (vuln.cpes ?? []).find((c) => /^cpe:2\.3:[aho]:/.test(c));
|
|
8146
8611
|
const pkg = buildAffectedPackage$1({
|
|
8147
8612
|
name: vuln.package_name,
|
|
@@ -8203,7 +8668,6 @@ async function convertNeuvectorToHdf(input, converterVersion = "1.0.0") {
|
|
|
8203
8668
|
generatorName: "neuvector-to-hdf",
|
|
8204
8669
|
converterVersion,
|
|
8205
8670
|
toolName: "NeuVector",
|
|
8206
|
-
toolFormat: "JSON",
|
|
8207
8671
|
baselines: [createMinimalBaseline("NeuVector Scan", requirements, {
|
|
8208
8672
|
resultsChecksum,
|
|
8209
8673
|
title: imageTitle(scan.report)
|
|
@@ -8222,7 +8686,9 @@ async function convertNeuvectorToHdf(input, converterVersion = "1.0.0") {
|
|
|
8222
8686
|
//#endregion
|
|
8223
8687
|
//#region converters/fortify-to-hdf/typescript/converter.ts
|
|
8224
8688
|
const NIST_REFERENCE_NAME = "Standards Mapping - NIST Special Publication 800-53 Revision 4";
|
|
8689
|
+
const CWE_REFERENCE_NAME = "Standards Mapping - Common Weakness Enumeration";
|
|
8225
8690
|
const NIST_PATTERN = /[a-zA-Z]{2}-\d+/g;
|
|
8691
|
+
const CWE_ID_PATTERN = /\d+/g;
|
|
8226
8692
|
const NAMED_ENTITIES$1 = {
|
|
8227
8693
|
lt: "<",
|
|
8228
8694
|
gt: ">",
|
|
@@ -8262,6 +8728,18 @@ function extractNISTFromReferences(refs) {
|
|
|
8262
8728
|
}
|
|
8263
8729
|
return [];
|
|
8264
8730
|
}
|
|
8731
|
+
function extractCWEFromReferences(refs) {
|
|
8732
|
+
for (const ref of refs) if (ref.Author === CWE_REFERENCE_NAME) {
|
|
8733
|
+
const matches = ref.Title?.match(CWE_ID_PATTERN);
|
|
8734
|
+
if (matches && matches.length > 0) return matches.map((m) => `CWE-${m}`);
|
|
8735
|
+
}
|
|
8736
|
+
return [];
|
|
8737
|
+
}
|
|
8738
|
+
function mergeCweNist(nist, cweIDs) {
|
|
8739
|
+
const merged = [...nist];
|
|
8740
|
+
for (const ctrl of mapCWEToNIST(cweIDs, [])) if (!merged.includes(ctrl)) merged.push(ctrl);
|
|
8741
|
+
return merged;
|
|
8742
|
+
}
|
|
8265
8743
|
function formatSnippet(snippet) {
|
|
8266
8744
|
const text = (snippet.Text ?? "").trim();
|
|
8267
8745
|
return `Path: ${snippet.File ?? ""}\nStartLine: ${snippet.StartLine ?? ""}, EndLine: ${snippet.EndLine ?? ""}\nCode:\n${text}`;
|
|
@@ -8284,8 +8762,26 @@ function buildCodeDesc$2(vuln, snippetMap) {
|
|
|
8284
8762
|
if (parts.length === 0) return `ClassID: ${vuln.ClassInfo?.ClassID ?? ""}, InstanceID: ${vuln.InstanceInfo?.InstanceID ?? ""}`;
|
|
8285
8763
|
return parts.join("\n");
|
|
8286
8764
|
}
|
|
8765
|
+
function buildRequirementCode(vulns, snippetMap) {
|
|
8766
|
+
if (vulns.length === 0) return void 0;
|
|
8767
|
+
const parts = [];
|
|
8768
|
+
const entries = ensureArray(vulns[0].AnalysisInfo?.Unified?.Trace?.Primary?.Entry);
|
|
8769
|
+
for (const entry of entries) {
|
|
8770
|
+
if (!entry.Node) continue;
|
|
8771
|
+
const snippetID = entry.Node.SourceLocation?.snippet;
|
|
8772
|
+
if (!snippetID) continue;
|
|
8773
|
+
const snippet = snippetMap.get(snippetID);
|
|
8774
|
+
if (!snippet) continue;
|
|
8775
|
+
const text = (snippet.Text ?? "").trim();
|
|
8776
|
+
if (text) parts.push(text);
|
|
8777
|
+
}
|
|
8778
|
+
if (parts.length === 0) return void 0;
|
|
8779
|
+
return parts.join("\n");
|
|
8780
|
+
}
|
|
8287
8781
|
function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
|
|
8288
|
-
|
|
8782
|
+
const refs = ensureArray(desc.References?.Reference);
|
|
8783
|
+
const cweIDs = extractCWEFromReferences(refs);
|
|
8784
|
+
let nistTags = mergeCweNist(extractNISTFromReferences(refs), cweIDs);
|
|
8289
8785
|
if (nistTags.length === 0) nistTags = [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS$1];
|
|
8290
8786
|
const cciTags = nistToCci(nistTags);
|
|
8291
8787
|
const tags = buildNistCciTags(nistTags, cciTags);
|
|
@@ -8301,7 +8797,7 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
|
|
|
8301
8797
|
data: stripFvdlMarkup(desc.Recommendations)
|
|
8302
8798
|
});
|
|
8303
8799
|
let impact = 0;
|
|
8304
|
-
if (vulns.length > 0) impact = parseFloat(vulns[0].
|
|
8800
|
+
if (vulns.length > 0) impact = parseFloat(vulns[0].InstanceInfo?.InstanceSeverity ?? "0") / 5;
|
|
8305
8801
|
const { items: limitedVulns, truncated } = limitArray(vulns);
|
|
8306
8802
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
8307
8803
|
if (truncated) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${vulns.length})`);
|
|
@@ -8319,8 +8815,11 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
|
|
|
8319
8815
|
results,
|
|
8320
8816
|
verificationMethod: VerificationMethodEnum.Automated
|
|
8321
8817
|
};
|
|
8818
|
+
if (cweIDs.length > 0) req.cwe = cweIDs;
|
|
8322
8819
|
const controlType = deriveControlTypeFromTags(nistTags);
|
|
8323
8820
|
if (controlType !== void 0) req.controlType = controlType;
|
|
8821
|
+
const code = buildRequirementCode(vulns, snippetMap);
|
|
8822
|
+
if (code !== void 0) req.code = code;
|
|
8324
8823
|
return req;
|
|
8325
8824
|
}
|
|
8326
8825
|
/**
|
|
@@ -8477,6 +8976,8 @@ function buildRequirement$4(rec, scanTime) {
|
|
|
8477
8976
|
const controlType = deriveControlTypeFromTags(nist);
|
|
8478
8977
|
if (controlType !== void 0) req.controlType = controlType;
|
|
8479
8978
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
8979
|
+
const cvss = buildCvssEntries(rec);
|
|
8980
|
+
if (cvss) req.cvss = cvss;
|
|
8480
8981
|
if (rec["CVE ID"]) {
|
|
8481
8982
|
const pkg = buildAffectedPackageFromRecord(rec);
|
|
8482
8983
|
if (pkg) req.affectedPackages = [pkg];
|
|
@@ -8484,6 +8985,32 @@ function buildRequirement$4(rec, scanTime) {
|
|
|
8484
8985
|
return req;
|
|
8485
8986
|
}
|
|
8486
8987
|
/**
|
|
8988
|
+
* Maps the CSV CVSS column to a structured cvss[] entry. Prisma emits a bare
|
|
8989
|
+
* base score with no vector, so version defaults to 3.1 via the shared helper.
|
|
8990
|
+
* Config rows carry "0.00" (or blank); only a positive score yields an entry,
|
|
8991
|
+
* and the source is the associated CVE when present.
|
|
8992
|
+
*/
|
|
8993
|
+
function buildCvssEntries(rec) {
|
|
8994
|
+
const score = parseCvssScore(rec.CVSS);
|
|
8995
|
+
if (score === void 0) return void 0;
|
|
8996
|
+
return [buildCvss$2({
|
|
8997
|
+
version: cvssVersionFromString(void 0),
|
|
8998
|
+
baseScore: score,
|
|
8999
|
+
source: rec["CVE ID"] || void 0
|
|
9000
|
+
})];
|
|
9001
|
+
}
|
|
9002
|
+
/**
|
|
9003
|
+
* Parses the CVSS column, returning undefined for blank, non-numeric, or
|
|
9004
|
+
* non-positive scores (the placeholder value on config rows).
|
|
9005
|
+
*/
|
|
9006
|
+
function parseCvssScore(field) {
|
|
9007
|
+
const s = field?.trim();
|
|
9008
|
+
if (!s) return void 0;
|
|
9009
|
+
const v = Number.parseFloat(s);
|
|
9010
|
+
if (!Number.isFinite(v) || v <= 0) return void 0;
|
|
9011
|
+
return v;
|
|
9012
|
+
}
|
|
9013
|
+
/**
|
|
8487
9014
|
* Distro slugs in Prisma look like `redhat-RHEL7`, `debian-buster`,
|
|
8488
9015
|
* `alpine-3.14`, `ubuntu-20.04`. Only the leading vendor segment is
|
|
8489
9016
|
* mapped — unknown vendors fall back to `generic` rather than guessing.
|
|
@@ -8576,7 +9103,6 @@ async function convertPrismaToHdf(input, converterVersion = "1.0.0") {
|
|
|
8576
9103
|
generatorName: "prisma-to-hdf",
|
|
8577
9104
|
converterVersion,
|
|
8578
9105
|
toolName: "Prisma Cloud",
|
|
8579
|
-
toolFormat: "CSV",
|
|
8580
9106
|
baselines,
|
|
8581
9107
|
components,
|
|
8582
9108
|
timestamp: scanTime
|
|
@@ -8633,6 +9159,36 @@ function formatControlDesc(vuln) {
|
|
|
8633
9159
|
return parts.join("\n");
|
|
8634
9160
|
}
|
|
8635
9161
|
/**
|
|
9162
|
+
* Returns the parsed Base-metric score from a CVSS block, or undefined when
|
|
9163
|
+
* there is no Base score or its value does not parse as a finite number.
|
|
9164
|
+
*/
|
|
9165
|
+
function baseScoreFromBlock(block) {
|
|
9166
|
+
const scores = block?.score ?? [];
|
|
9167
|
+
for (const s of scores) if ((s.type ?? "").toLowerCase() === "base") {
|
|
9168
|
+
const v = parseFloat((s.value ?? "").trim());
|
|
9169
|
+
return Number.isFinite(v) ? v : void 0;
|
|
9170
|
+
}
|
|
9171
|
+
}
|
|
9172
|
+
/**
|
|
9173
|
+
* Assembles the structured cvss[] from the <cvss> (3.0) and <cvss31> (3.1)
|
|
9174
|
+
* classification blocks. Each block carrying a vector or a Base score becomes
|
|
9175
|
+
* one entry; the schema version derives from the vector prefix.
|
|
9176
|
+
*/
|
|
9177
|
+
function buildNetsparkerCvss(classification) {
|
|
9178
|
+
const out = [];
|
|
9179
|
+
for (const block of [classification?.cvss, classification?.cvss31]) {
|
|
9180
|
+
const baseScore = baseScoreFromBlock(block);
|
|
9181
|
+
const vector = block?.vector ?? "";
|
|
9182
|
+
if (!vector && baseScore === void 0) continue;
|
|
9183
|
+
out.push(buildCvss$2({
|
|
9184
|
+
version: cvssVersionFromVector(vector),
|
|
9185
|
+
baseScore,
|
|
9186
|
+
baseVector: vector
|
|
9187
|
+
}));
|
|
9188
|
+
}
|
|
9189
|
+
return out;
|
|
9190
|
+
}
|
|
9191
|
+
/**
|
|
8636
9192
|
* Performs dual NIST mapping from both CWE and OWASP IDs.
|
|
8637
9193
|
* Returns a deduplicated sorted list of NIST controls, falling back to
|
|
8638
9194
|
* DEFAULT_STATIC_ANALYSIS_NIST_TAGS if no mappings are found.
|
|
@@ -8704,6 +9260,10 @@ function buildRequirement$3(vuln, initiated) {
|
|
|
8704
9260
|
const controlType = deriveControlTypeFromTags(nist);
|
|
8705
9261
|
if (controlType !== void 0) req.controlType = controlType;
|
|
8706
9262
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
9263
|
+
const rawRequest = vuln["http-request"]?.content;
|
|
9264
|
+
if (rawRequest) req.code = rawRequest;
|
|
9265
|
+
const cvss = buildNetsparkerCvss(vuln.classification);
|
|
9266
|
+
if (cvss.length > 0) req.cvss = cvss;
|
|
8707
9267
|
return req;
|
|
8708
9268
|
}
|
|
8709
9269
|
/**
|
|
@@ -8717,7 +9277,7 @@ async function convertNetsparkerToHdf(input, converterVersion = "1.0.0") {
|
|
|
8717
9277
|
if (!input || input.trim().length === 0) throw new Error("netsparker: empty input");
|
|
8718
9278
|
validateInputSize(input, "netsparker");
|
|
8719
9279
|
const resultsChecksum = await inputChecksum(input);
|
|
8720
|
-
const parsed = parseXmlWithArrays(input, ["vulnerability"]);
|
|
9280
|
+
const parsed = parseXmlWithArrays(input, ["vulnerability", "score"]);
|
|
8721
9281
|
const isInvicti = !!parsed["invicti-enterprise"];
|
|
8722
9282
|
const data = parsed["invicti-enterprise"] ?? parsed["netsparker-enterprise"] ?? {};
|
|
8723
9283
|
if (!data.vulnerabilities && !data.target) throw new Error("netsparker: invalid XML — missing expected root element");
|
|
@@ -8738,7 +9298,6 @@ async function convertNetsparkerToHdf(input, converterVersion = "1.0.0") {
|
|
|
8738
9298
|
generatorName: "netsparker-to-hdf",
|
|
8739
9299
|
converterVersion,
|
|
8740
9300
|
toolName,
|
|
8741
|
-
toolFormat: "XML",
|
|
8742
9301
|
baselines: [createMinimalBaseline("Netsparker Scan", requirements, {
|
|
8743
9302
|
resultsChecksum,
|
|
8744
9303
|
title: `${toolName} Enterprise Scan ID: ${target["scan-id"] ?? ""} URL: ${target.url ?? ""}`
|
|
@@ -8872,7 +9431,6 @@ async function convertScoutsuiteToHdf(input, converterVersion = "1.0.0") {
|
|
|
8872
9431
|
generatorName: "scoutsuite-to-hdf",
|
|
8873
9432
|
converterVersion,
|
|
8874
9433
|
toolName: "ScoutSuite",
|
|
8875
|
-
toolFormat: "JSON",
|
|
8876
9434
|
toolVersion: report.last_run.version,
|
|
8877
9435
|
baselines: [baseline],
|
|
8878
9436
|
components: [{
|
|
@@ -9036,7 +9594,6 @@ async function convertConveyorToHdf(input, converterVersion = "1.0.0") {
|
|
|
9036
9594
|
generatorName: "conveyor-to-hdf",
|
|
9037
9595
|
converterVersion,
|
|
9038
9596
|
toolName: "Conveyor",
|
|
9039
|
-
toolFormat: "JSON",
|
|
9040
9597
|
baselines,
|
|
9041
9598
|
components: [{
|
|
9042
9599
|
name: targetName,
|
|
@@ -9118,23 +9675,6 @@ function formatRecommendations(rec) {
|
|
|
9118
9675
|
}
|
|
9119
9676
|
return parts.join("\n");
|
|
9120
9677
|
}
|
|
9121
|
-
/** Format CWE data for the cweid tag. */
|
|
9122
|
-
function formatCWEData(cwes) {
|
|
9123
|
-
return cwes.map((c) => {
|
|
9124
|
-
let entry = `CWE-${attr(c, "cweid")}: ${attr(c, "cwename")}`;
|
|
9125
|
-
const categories = [
|
|
9126
|
-
["pcrirelated", attr(c, "pcirelated")],
|
|
9127
|
-
["owasp", attr(c, "owasp")],
|
|
9128
|
-
["sans", attr(c, "sans")],
|
|
9129
|
-
["certc", attr(c, "certc")],
|
|
9130
|
-
["certcpp", attr(c, "certcpp")],
|
|
9131
|
-
["certjava", attr(c, "certjava")],
|
|
9132
|
-
["owaspmobile", attr(c, "owaspmobile")]
|
|
9133
|
-
];
|
|
9134
|
-
for (const [name, val] of categories) if (val) entry += `${name}: ${val}\n`;
|
|
9135
|
-
return entry;
|
|
9136
|
-
}).join("\n");
|
|
9137
|
-
}
|
|
9138
9678
|
/** Format CWE descriptions for the cweDescription tag. */
|
|
9139
9679
|
function formatCWEDesc(cwes) {
|
|
9140
9680
|
return cwes.map((c) => {
|
|
@@ -9195,6 +9735,54 @@ function formatSCACodeDesc(comp) {
|
|
|
9195
9735
|
}
|
|
9196
9736
|
return parts.join("\n");
|
|
9197
9737
|
}
|
|
9738
|
+
/**
|
|
9739
|
+
* Synthesize a static flaw's source-context locus from its function prototype
|
|
9740
|
+
* and source-file position. Returns '' when the flaw carries neither a prototype
|
|
9741
|
+
* nor a source location (the NOT-IN-SOURCE case).
|
|
9742
|
+
*/
|
|
9743
|
+
function synthesizeFlawCode(flaw) {
|
|
9744
|
+
let locus = attr(flaw, "sourcefilepath") + attr(flaw, "sourcefile");
|
|
9745
|
+
const line = attr(flaw, "line");
|
|
9746
|
+
if (locus && line) locus += `:${line}`;
|
|
9747
|
+
const proto = attr(flaw, "functionprototype");
|
|
9748
|
+
if (proto && locus) return `${proto} at ${locus}`;
|
|
9749
|
+
if (proto) return proto;
|
|
9750
|
+
return locus;
|
|
9751
|
+
}
|
|
9752
|
+
/**
|
|
9753
|
+
* Serialize a CVE and its affected components as indented JSON. Field order is
|
|
9754
|
+
* load-bearing: it must match the Go twin's struct declaration order so the two
|
|
9755
|
+
* `code` strings are byte-identical.
|
|
9756
|
+
*/
|
|
9757
|
+
function buildSCACode(vuln, components) {
|
|
9758
|
+
const entry = {
|
|
9759
|
+
cve_id: attr(vuln, "cve_id"),
|
|
9760
|
+
cvss_score: attr(vuln, "cvss_score"),
|
|
9761
|
+
severity: attr(vuln, "severity"),
|
|
9762
|
+
cwe_id: attr(vuln, "cwe_id"),
|
|
9763
|
+
first_found_date: attr(vuln, "first_found_date"),
|
|
9764
|
+
cve_summary: attr(vuln, "cve_summary"),
|
|
9765
|
+
severity_desc: attr(vuln, "severity_desc"),
|
|
9766
|
+
components: components.map((comp) => {
|
|
9767
|
+
const filePathsElem = comp.file_paths;
|
|
9768
|
+
const fps = filePathsElem ? ensureArray(filePathsElem.file_path) : [];
|
|
9769
|
+
return {
|
|
9770
|
+
component_id: attr(comp, "component_id"),
|
|
9771
|
+
file_name: attr(comp, "file_name"),
|
|
9772
|
+
sha1: attr(comp, "sha1"),
|
|
9773
|
+
version: attr(comp, "version"),
|
|
9774
|
+
library: attr(comp, "library"),
|
|
9775
|
+
library_id: attr(comp, "library_id"),
|
|
9776
|
+
vendor: attr(comp, "vendor"),
|
|
9777
|
+
description: attr(comp, "description"),
|
|
9778
|
+
max_cvss_score: attr(comp, "max_cvss_score"),
|
|
9779
|
+
added_date: attr(comp, "added_date"),
|
|
9780
|
+
file_paths: fps.map((fp) => attr(fp, "value"))
|
|
9781
|
+
};
|
|
9782
|
+
})
|
|
9783
|
+
};
|
|
9784
|
+
return JSON.stringify(entry, null, 2);
|
|
9785
|
+
}
|
|
9198
9786
|
/** Build CWE-based requirements from severity categories. */
|
|
9199
9787
|
function buildCWERequirements(severities, firstBuildDate) {
|
|
9200
9788
|
const requirements = [];
|
|
@@ -9208,14 +9796,14 @@ function buildCWERequirements(severities, firstBuildDate) {
|
|
|
9208
9796
|
/** Build a single CWE-based requirement from a category. */
|
|
9209
9797
|
function buildCWERequirement(cat, impact, firstBuildDate) {
|
|
9210
9798
|
const cwes = ensureArray(cat.cwe);
|
|
9211
|
-
const
|
|
9799
|
+
const cweIDs = cwes.map((c) => attr(c, "cweid")).filter(Boolean);
|
|
9800
|
+
const nist = mapCWEToNIST(cweIDs, DEFAULT_REMEDIATION_NIST_TAGS$1);
|
|
9212
9801
|
const cciTags = nistToCci(nist);
|
|
9213
9802
|
const extras = {};
|
|
9214
|
-
const cweData = formatCWEData(cwes);
|
|
9215
|
-
if (cweData) extras.cweid = cweData;
|
|
9216
9803
|
const cweDescStr = formatCWEDesc(cwes);
|
|
9217
9804
|
if (cweDescStr) extras.cweDescription = cweDescStr;
|
|
9218
9805
|
const tags = buildNistCciTags(nist, cciTags, extras);
|
|
9806
|
+
const cweList = cweIDs.map((id) => `CWE-${id}`);
|
|
9219
9807
|
const descriptions = [{
|
|
9220
9808
|
label: "default",
|
|
9221
9809
|
data: formatDesc(cat.desc)
|
|
@@ -9237,6 +9825,10 @@ function buildCWERequirement(cat, impact, firstBuildDate) {
|
|
|
9237
9825
|
const staticflaws = c.staticflaws;
|
|
9238
9826
|
return ensureArray(staticflaws?.flaw).map((flaw) => attr(flaw, "sourcefile")).filter(Boolean);
|
|
9239
9827
|
}).join("\n");
|
|
9828
|
+
const codeLines = cwes.flatMap((c) => {
|
|
9829
|
+
const staticflaws = c.staticflaws;
|
|
9830
|
+
return ensureArray(staticflaws?.flaw).map((flaw) => synthesizeFlawCode(flaw)).filter(Boolean);
|
|
9831
|
+
});
|
|
9240
9832
|
const req = createRequirement(attr(cat, "categoryid"), attr(cat, "categoryname"), descriptions, impact, results, {
|
|
9241
9833
|
tags,
|
|
9242
9834
|
sourceLocation: sourceRef ? { ref: sourceRef } : void 0
|
|
@@ -9244,6 +9836,8 @@ function buildCWERequirement(cat, impact, firstBuildDate) {
|
|
|
9244
9836
|
const controlType = deriveControlTypeFromTags(nist);
|
|
9245
9837
|
if (controlType !== void 0) req.controlType = controlType;
|
|
9246
9838
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
9839
|
+
if (cweList.length > 0) req.cwe = cweList;
|
|
9840
|
+
if (codeLines.length > 0) req.code = codeLines.join("\n");
|
|
9247
9841
|
return req;
|
|
9248
9842
|
}
|
|
9249
9843
|
/** Build CVE-based requirements from SCA components. */
|
|
@@ -9278,6 +9872,30 @@ function buildCVERequirements(sca, firstBuildDate) {
|
|
|
9278
9872
|
return buildCVERequirement(entry.vuln, entry.components, firstBuildDate);
|
|
9279
9873
|
});
|
|
9280
9874
|
}
|
|
9875
|
+
/**
|
|
9876
|
+
* Assemble the structured CVSS entry for an SCA CVE. Veracode reports a bare
|
|
9877
|
+
* numeric base score (no vector, no version), so the version defaults to 3.1 via
|
|
9878
|
+
* the shared helper. When the vulnerability itself carries no cvss_score, the
|
|
9879
|
+
* first affected component's max_cvss_score is used as a fallback. A missing or
|
|
9880
|
+
* non-numeric score yields no entry.
|
|
9881
|
+
*/
|
|
9882
|
+
function buildVeracodeCvss(vuln, components) {
|
|
9883
|
+
let scoreStr = attr(vuln, "cvss_score");
|
|
9884
|
+
if (!scoreStr) for (const comp of components) {
|
|
9885
|
+
const max = attr(comp, "max_cvss_score");
|
|
9886
|
+
if (max) {
|
|
9887
|
+
scoreStr = max;
|
|
9888
|
+
break;
|
|
9889
|
+
}
|
|
9890
|
+
}
|
|
9891
|
+
if (!scoreStr) return [];
|
|
9892
|
+
const score = Number(scoreStr);
|
|
9893
|
+
if (!Number.isFinite(score)) return [];
|
|
9894
|
+
return [buildCvss$2({
|
|
9895
|
+
version: cvssVersionFromString(void 0),
|
|
9896
|
+
baseScore: score
|
|
9897
|
+
})];
|
|
9898
|
+
}
|
|
9281
9899
|
/** Build a single CVE-based requirement. */
|
|
9282
9900
|
function buildCVERequirement(vuln, components, firstBuildDate) {
|
|
9283
9901
|
const impact = veracodeSeverityToImpact(attr(vuln, "severity"));
|
|
@@ -9286,9 +9904,7 @@ function buildCVERequirement(vuln, components, firstBuildDate) {
|
|
|
9286
9904
|
if (cweID) nist = mapCWEToNIST([cweID.replace(/^CWE-/, "")], DEFAULT_REMEDIATION_NIST_TAGS$1);
|
|
9287
9905
|
else nist = DEFAULT_REMEDIATION_NIST_TAGS$1;
|
|
9288
9906
|
const cciTags = nistToCci(nist);
|
|
9289
|
-
const
|
|
9290
|
-
if (cweID) extras.cwe = cweID;
|
|
9291
|
-
const tags = buildNistCciTags(nist, cciTags, extras);
|
|
9907
|
+
const tags = buildNistCciTags(nist, cciTags, {});
|
|
9292
9908
|
const startTime = parseVeracodeTimestamp(firstBuildDate) ?? /* @__PURE__ */ new Date();
|
|
9293
9909
|
const results = components.map((comp) => createResult(ResultStatus.Failed, void 0, {
|
|
9294
9910
|
codeDesc: formatSCACodeDesc(comp),
|
|
@@ -9312,6 +9928,10 @@ function buildCVERequirement(vuln, components, firstBuildDate) {
|
|
|
9312
9928
|
const controlType = deriveControlTypeFromTags(nist);
|
|
9313
9929
|
if (controlType !== void 0) req.controlType = controlType;
|
|
9314
9930
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
9931
|
+
const cvss = buildVeracodeCvss(vuln, components);
|
|
9932
|
+
if (cvss.length > 0) req.cvss = cvss;
|
|
9933
|
+
if (cweID) req.cwe = [cweID];
|
|
9934
|
+
req.code = buildSCACode(vuln, components);
|
|
9315
9935
|
return req;
|
|
9316
9936
|
}
|
|
9317
9937
|
/**
|
|
@@ -9357,7 +9977,6 @@ async function convertVeracodeToHdf(input, converterVersion = "1.0.0") {
|
|
|
9357
9977
|
generatorName: "veracode-to-hdf",
|
|
9358
9978
|
converterVersion,
|
|
9359
9979
|
toolName: "Veracode",
|
|
9360
|
-
toolFormat: "XML",
|
|
9361
9980
|
baselines: [baseline],
|
|
9362
9981
|
components: [{
|
|
9363
9982
|
name: targetName,
|
|
@@ -9465,7 +10084,6 @@ async function convertMsftSecureScoreToHdf(input, converterVersion = "1.0.0") {
|
|
|
9465
10084
|
generatorName: "msft-secure-score-to-hdf",
|
|
9466
10085
|
converterVersion,
|
|
9467
10086
|
toolName: "Microsoft Secure Score",
|
|
9468
|
-
toolFormat: "JSON",
|
|
9469
10087
|
baselines: combined.secureScore.value.map((ss) => {
|
|
9470
10088
|
if (!tenantId) tenantId = ss.azureTenantId;
|
|
9471
10089
|
const { items: limitedControlScores, truncated } = limitArray(ss.controlScores);
|
|
@@ -9701,7 +10319,6 @@ async function convertMsftDefenderCloudToHdf(input, converterVersion = "1.0.0")
|
|
|
9701
10319
|
generatorName: "msft-defender-cloud-to-hdf",
|
|
9702
10320
|
converterVersion,
|
|
9703
10321
|
toolName: "Microsoft Defender for Cloud",
|
|
9704
|
-
toolFormat: "JSON",
|
|
9705
10322
|
baselines: [baseline],
|
|
9706
10323
|
components,
|
|
9707
10324
|
timestamp: scanTime
|
|
@@ -9977,8 +10594,6 @@ function buildRuleObj(req) {
|
|
|
9977
10594
|
}
|
|
9978
10595
|
const rationale = findDescription(req.descriptions, "rationale");
|
|
9979
10596
|
if (rationale) rule.rationale = wrap(rationale);
|
|
9980
|
-
const fixtext = findDescription(req.descriptions, "fix");
|
|
9981
|
-
if (fixtext) rule.fixtext = wrap(fixtext);
|
|
9982
10597
|
const idents = [];
|
|
9983
10598
|
if (req.tags && Array.isArray(req.tags["cci"])) for (const cci of req.tags["cci"]) idents.push({
|
|
9984
10599
|
[`${ATTR}system`]: "http://cyber.mil/cci",
|
|
@@ -9989,6 +10604,8 @@ function buildRuleObj(req) {
|
|
|
9989
10604
|
"#text": n
|
|
9990
10605
|
});
|
|
9991
10606
|
if (idents.length > 0) rule.ident = idents;
|
|
10607
|
+
const fixtext = findDescription(req.descriptions, "fix");
|
|
10608
|
+
if (fixtext) rule.fixtext = wrap(fixtext);
|
|
9992
10609
|
const checks = [];
|
|
9993
10610
|
const checkContent = findDescription(req.descriptions, "check");
|
|
9994
10611
|
if (checkContent) checks.push({
|
|
@@ -10019,12 +10636,19 @@ function buildTestResultObj(hdfData, baseline) {
|
|
|
10019
10636
|
if (target.ipAddress) testResult["target-address"] = wrap(target.ipAddress);
|
|
10020
10637
|
} else testResult.target = wrap("unknown");
|
|
10021
10638
|
const ruleResults = [];
|
|
10639
|
+
let passed = 0;
|
|
10640
|
+
let scorable = 0;
|
|
10022
10641
|
for (const req of baseline.requirements) {
|
|
10023
10642
|
const ruleIdRef = sanitizeXccdfId("xccdf_hdf_rule_" + req.id + "_rule");
|
|
10024
10643
|
for (const result of req.results) {
|
|
10644
|
+
const status = hdfStatusToXccdf(result.status);
|
|
10645
|
+
if (status === "pass") {
|
|
10646
|
+
passed++;
|
|
10647
|
+
scorable++;
|
|
10648
|
+
} else if (status === "fail") scorable++;
|
|
10025
10649
|
const rr = {
|
|
10026
10650
|
[`${ATTR}idref`]: ruleIdRef,
|
|
10027
|
-
result: wrap(
|
|
10651
|
+
result: wrap(status)
|
|
10028
10652
|
};
|
|
10029
10653
|
rr[`${ATTR}time`] = typeof result.startTime === "string" ? result.startTime : result.startTime.toISOString();
|
|
10030
10654
|
if (result.message) rr.message = wrap(result.message);
|
|
@@ -10036,6 +10660,12 @@ function buildTestResultObj(hdfData, baseline) {
|
|
|
10036
10660
|
}
|
|
10037
10661
|
}
|
|
10038
10662
|
testResult["rule-result"] = ruleResults;
|
|
10663
|
+
const score = scorable > 0 ? passed / scorable * 100 : 0;
|
|
10664
|
+
testResult.score = {
|
|
10665
|
+
[`${ATTR}system`]: "urn:xccdf:scoring:default",
|
|
10666
|
+
[`${ATTR}maximum`]: "100.000000",
|
|
10667
|
+
"#text": score.toFixed(6)
|
|
10668
|
+
};
|
|
10039
10669
|
return testResult;
|
|
10040
10670
|
}
|
|
10041
10671
|
//#endregion
|
|
@@ -10228,23 +10858,40 @@ async function convertHdfToOscalSar(input) {
|
|
|
10228
10858
|
return JSON.stringify(doc, null, 2);
|
|
10229
10859
|
}
|
|
10230
10860
|
/**
|
|
10861
|
+
* Human-readable assessment-tool label from the HDF tool/generator identity,
|
|
10862
|
+
* falling back when neither is present.
|
|
10863
|
+
*/
|
|
10864
|
+
function toolPartyName(hdfResults) {
|
|
10865
|
+
const tool = hdfResults.tool;
|
|
10866
|
+
if (tool?.name) return tool.version ? `${tool.name} ${tool.version}` : tool.name;
|
|
10867
|
+
const gen = hdfResults.generator;
|
|
10868
|
+
if (gen?.name) return gen.version ? `${gen.name} ${gen.version}` : gen.name;
|
|
10869
|
+
return "HDF Assessment Tool";
|
|
10870
|
+
}
|
|
10871
|
+
/**
|
|
10231
10872
|
* Constructs the full OSCAL assessment-results document from HDF results.
|
|
10232
10873
|
*/
|
|
10233
10874
|
function buildOSCALDocument(hdfResults) {
|
|
10234
10875
|
let timestamp = formatTimestampSeconds(/* @__PURE__ */ new Date());
|
|
10235
10876
|
const documentTime = hdfTime(hdfResults.timestamp);
|
|
10236
10877
|
if (documentTime) timestamp = formatTimestampSeconds(documentTime);
|
|
10878
|
+
const toolActorUuid = crypto.randomUUID();
|
|
10237
10879
|
const metadata = {
|
|
10238
10880
|
title: "HDF Assessment Results Export",
|
|
10239
10881
|
"last-modified": timestamp,
|
|
10240
10882
|
version: "1.0.0",
|
|
10241
|
-
"oscal-version": OSCAL_VERSION
|
|
10883
|
+
"oscal-version": OSCAL_VERSION,
|
|
10884
|
+
parties: [{
|
|
10885
|
+
uuid: toolActorUuid,
|
|
10886
|
+
type: "organization",
|
|
10887
|
+
name: toolPartyName(hdfResults)
|
|
10888
|
+
}]
|
|
10242
10889
|
};
|
|
10243
10890
|
let importAP;
|
|
10244
10891
|
if (hdfResults.planRef && hdfResults.planRef !== "") importAP = { href: hdfResults.planRef };
|
|
10245
10892
|
else importAP = { href: "#" };
|
|
10246
10893
|
const results = [];
|
|
10247
|
-
for (const baseline of hdfResults.baselines) results.push(baselineToResult(baseline, timestamp));
|
|
10894
|
+
for (const baseline of hdfResults.baselines) results.push(baselineToResult(baseline, timestamp, toolActorUuid));
|
|
10248
10895
|
return { "assessment-results": {
|
|
10249
10896
|
uuid: crypto.randomUUID(),
|
|
10250
10897
|
metadata,
|
|
@@ -10283,7 +10930,7 @@ function assessmentStart(baseline, fallback) {
|
|
|
10283
10930
|
/**
|
|
10284
10931
|
* Converts a single EvaluatedBaseline to an OSCAL Result.
|
|
10285
10932
|
*/
|
|
10286
|
-
function baselineToResult(baseline, timestamp) {
|
|
10933
|
+
function baselineToResult(baseline, timestamp, toolActorUuid) {
|
|
10287
10934
|
let title = baseline.name;
|
|
10288
10935
|
if (baseline.title && baseline.title !== "") title = baseline.title;
|
|
10289
10936
|
let description = "Converted from HDF results";
|
|
@@ -10291,17 +10938,26 @@ function baselineToResult(baseline, timestamp) {
|
|
|
10291
10938
|
const findings = [];
|
|
10292
10939
|
const observations = [];
|
|
10293
10940
|
const risks = [];
|
|
10941
|
+
const includeControls = [];
|
|
10942
|
+
const seenControl = /* @__PURE__ */ new Set();
|
|
10294
10943
|
for (const req of baseline.requirements) {
|
|
10295
|
-
const { finding, observation, risk } = requirementToFindingSet(req, timestamp);
|
|
10944
|
+
const { finding, observation, risk } = requirementToFindingSet(req, timestamp, toolActorUuid);
|
|
10296
10945
|
findings.push(finding);
|
|
10297
10946
|
if (observation) observations.push(observation);
|
|
10298
10947
|
if (risk) risks.push(risk);
|
|
10948
|
+
const cid = nistTagToControlId(req.id);
|
|
10949
|
+
if (cid !== "" && !seenControl.has(cid)) {
|
|
10950
|
+
seenControl.add(cid);
|
|
10951
|
+
includeControls.push({ "control-id": cid });
|
|
10952
|
+
}
|
|
10299
10953
|
}
|
|
10954
|
+
const controlSelection = includeControls.length > 0 ? { "include-controls": includeControls } : {};
|
|
10300
10955
|
return {
|
|
10301
10956
|
uuid: crypto.randomUUID(),
|
|
10302
10957
|
title,
|
|
10303
10958
|
description,
|
|
10304
10959
|
start: assessmentStart(baseline, timestamp),
|
|
10960
|
+
"reviewed-controls": { "control-selections": [controlSelection] },
|
|
10305
10961
|
findings,
|
|
10306
10962
|
observations,
|
|
10307
10963
|
risks
|
|
@@ -10310,49 +10966,39 @@ function baselineToResult(baseline, timestamp) {
|
|
|
10310
10966
|
/**
|
|
10311
10967
|
* Converts an EvaluatedRequirement into a Finding, optional Observation, and optional Risk.
|
|
10312
10968
|
*/
|
|
10313
|
-
function requirementToFindingSet(req, timestamp) {
|
|
10969
|
+
function requirementToFindingSet(req, timestamp, toolActorUuid) {
|
|
10314
10970
|
const controlID = nistTagToControlId(req.id);
|
|
10315
|
-
const
|
|
10316
|
-
const
|
|
10971
|
+
const results = req.results ?? [];
|
|
10972
|
+
const descriptions = req.descriptions ?? [];
|
|
10973
|
+
const { state, reason } = aggregateStatus(results);
|
|
10974
|
+
const findingDesc = extractDefaultDescription(descriptions);
|
|
10317
10975
|
const props = [];
|
|
10976
|
+
const addProp = (name, value) => {
|
|
10977
|
+
if (value !== "") props.push({
|
|
10978
|
+
name,
|
|
10979
|
+
value
|
|
10980
|
+
});
|
|
10981
|
+
};
|
|
10318
10982
|
const pushTagValues = (key) => {
|
|
10319
10983
|
const raw = req.tags?.[key];
|
|
10320
10984
|
if (Array.isArray(raw)) {
|
|
10321
|
-
for (const v of raw) if (typeof v === "string")
|
|
10322
|
-
name: key,
|
|
10323
|
-
value: v
|
|
10324
|
-
});
|
|
10985
|
+
for (const v of raw) if (typeof v === "string") addProp(key, v);
|
|
10325
10986
|
}
|
|
10326
10987
|
};
|
|
10327
10988
|
pushTagValues("nist");
|
|
10328
10989
|
pushTagValues("cci");
|
|
10329
|
-
if (req.code)
|
|
10330
|
-
name: "code",
|
|
10331
|
-
value: req.code
|
|
10332
|
-
});
|
|
10990
|
+
if (req.code != null) addProp("code", req.code);
|
|
10333
10991
|
for (const label of [
|
|
10334
10992
|
"check",
|
|
10335
10993
|
"fix",
|
|
10336
10994
|
"rationale"
|
|
10337
10995
|
]) {
|
|
10338
|
-
const d =
|
|
10339
|
-
|
|
10340
|
-
name: label,
|
|
10341
|
-
value: d.data
|
|
10342
|
-
});
|
|
10996
|
+
const d = descriptions.find((x) => x.label === label);
|
|
10997
|
+
addProp(label, d ? d.data : "");
|
|
10343
10998
|
}
|
|
10344
|
-
if (req.controlType)
|
|
10345
|
-
|
|
10346
|
-
|
|
10347
|
-
});
|
|
10348
|
-
if (req.verificationMethod) props.push({
|
|
10349
|
-
name: "verification-method",
|
|
10350
|
-
value: req.verificationMethod
|
|
10351
|
-
});
|
|
10352
|
-
if (req.applicability) props.push({
|
|
10353
|
-
name: "applicability",
|
|
10354
|
-
value: req.applicability
|
|
10355
|
-
});
|
|
10999
|
+
if (req.controlType) addProp("control-type", req.controlType);
|
|
11000
|
+
if (req.verificationMethod) addProp("verification-method", req.verificationMethod);
|
|
11001
|
+
if (req.applicability) addProp("applicability", req.applicability);
|
|
10356
11002
|
const links = [];
|
|
10357
11003
|
if (Array.isArray(req.refs)) for (const r of req.refs) {
|
|
10358
11004
|
const o = r;
|
|
@@ -10381,19 +11027,19 @@ function requirementToFindingSet(req, timestamp) {
|
|
|
10381
11027
|
const finding = {
|
|
10382
11028
|
uuid: crypto.randomUUID(),
|
|
10383
11029
|
title,
|
|
10384
|
-
description: findingDesc ||
|
|
11030
|
+
description: findingDesc || title,
|
|
10385
11031
|
props: props.length > 0 ? props : void 0,
|
|
10386
11032
|
links: links.length > 0 ? links : void 0,
|
|
10387
11033
|
target
|
|
10388
11034
|
};
|
|
10389
11035
|
let observation;
|
|
10390
|
-
if (
|
|
11036
|
+
if (results.length > 0) {
|
|
10391
11037
|
const obsUUID = crypto.randomUUID();
|
|
10392
11038
|
observation = {
|
|
10393
11039
|
uuid: obsUUID,
|
|
10394
|
-
description: buildObservationDescription(
|
|
11040
|
+
description: buildObservationDescription(results),
|
|
10395
11041
|
methods: ["TEST"],
|
|
10396
|
-
collected: formatAssessmentTime(earliestResultTime(
|
|
11042
|
+
collected: formatAssessmentTime(earliestResultTime(results), timestamp)
|
|
10397
11043
|
};
|
|
10398
11044
|
finding["related-observations"] = [{ "observation-uuid": obsUUID }];
|
|
10399
11045
|
}
|
|
@@ -10407,11 +11053,17 @@ function requirementToFindingSet(req, timestamp) {
|
|
|
10407
11053
|
description: `Impact: ${req.impact.toFixed(1)} (${severity})`,
|
|
10408
11054
|
statement: `Impact: ${req.impact.toFixed(1)} (${severity})`,
|
|
10409
11055
|
status: riskStatusFromState(state),
|
|
10410
|
-
characterizations: [{
|
|
10411
|
-
|
|
10412
|
-
|
|
10413
|
-
|
|
10414
|
-
|
|
11056
|
+
characterizations: [{
|
|
11057
|
+
origin: { actors: [{
|
|
11058
|
+
type: "party",
|
|
11059
|
+
"actor-uuid": toolActorUuid
|
|
11060
|
+
}] },
|
|
11061
|
+
facets: [{
|
|
11062
|
+
name: "impact",
|
|
11063
|
+
system: "https://fedramp.gov",
|
|
11064
|
+
value: severity
|
|
11065
|
+
}]
|
|
11066
|
+
}]
|
|
10415
11067
|
};
|
|
10416
11068
|
finding["related-risks"] = [{ "risk-uuid": riskUUID }];
|
|
10417
11069
|
}
|
|
@@ -10711,15 +11363,56 @@ function buildTags(dep) {
|
|
|
10711
11363
|
if (dep.parentDependencies.length > 0) extras.parentDependencies = dep.parentDependencies;
|
|
10712
11364
|
return buildNistCciTags(nist, cciTags, extras);
|
|
10713
11365
|
}
|
|
11366
|
+
function titleCaseFirst(s) {
|
|
11367
|
+
return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
11368
|
+
}
|
|
11369
|
+
function buildScanRequirement(scan) {
|
|
11370
|
+
const title = scan.description || `${titleCaseFirst(scan.name)} scan`;
|
|
11371
|
+
const descriptions = [createDescription("default", scan.summary || `${titleCaseFirst(scan.name)} scan summary`)];
|
|
11372
|
+
const results = [{
|
|
11373
|
+
status: ResultStatus.NotReviewed,
|
|
11374
|
+
codeDesc: `${scan.name} scan summary`,
|
|
11375
|
+
startTime: /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
|
|
11376
|
+
}];
|
|
11377
|
+
const req = createRequirement(`scan-${scan.name}`, title, descriptions, 0, results, { tags: {
|
|
11378
|
+
name: scan.name,
|
|
11379
|
+
type: scan.results?.type ?? ""
|
|
11380
|
+
} });
|
|
11381
|
+
req.code = JSON.stringify(scan.results?.data ?? {}, null, 2);
|
|
11382
|
+
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
11383
|
+
return req;
|
|
11384
|
+
}
|
|
11385
|
+
function verdictDescription(a) {
|
|
11386
|
+
let desc = `Ion Channel analysis verdict: ${a.passed ? "PASSED" : "FAILED"}`;
|
|
11387
|
+
if (a.risk) desc += ` (risk: ${a.risk})`;
|
|
11388
|
+
if (a.ruleset_name) {
|
|
11389
|
+
desc += `. Ruleset: ${a.ruleset_name}`;
|
|
11390
|
+
if (a.ruleset_id) desc += ` (${a.ruleset_id})`;
|
|
11391
|
+
}
|
|
11392
|
+
return `${desc}.`;
|
|
11393
|
+
}
|
|
11394
|
+
function verdictLabels(a) {
|
|
11395
|
+
const labels = { passed: String(a.passed) };
|
|
11396
|
+
if (a.risk) labels.risk = a.risk;
|
|
11397
|
+
if (a.ruleset_name) labels.ruleset_name = a.ruleset_name;
|
|
11398
|
+
if (a.ruleset_id) labels.ruleset_id = a.ruleset_id;
|
|
11399
|
+
return labels;
|
|
11400
|
+
}
|
|
10714
11401
|
async function convertIonchannelToHdf(input, converterVersion = "1.0.0") {
|
|
10715
11402
|
if (!input?.trim()) throw new Error("Empty input");
|
|
10716
11403
|
validateInputSize(input, "ionchannel");
|
|
10717
11404
|
const analysis = parseJSON(input);
|
|
10718
11405
|
if (!Array.isArray(analysis.scan_summaries)) throw new Error(`Ion Channel scan_summaries invalid summary data (expecting array, got ${typeof analysis.scan_summaries})`);
|
|
10719
11406
|
let allDeps = [];
|
|
10720
|
-
|
|
10721
|
-
|
|
10722
|
-
|
|
11407
|
+
const nonDepScans = [];
|
|
11408
|
+
let foundDep = false;
|
|
11409
|
+
for (const scan of analysis.scan_summaries) {
|
|
11410
|
+
if (scan.name === "dependency" && !foundDep) {
|
|
11411
|
+
allDeps = scan.results?.data?.dependencies ?? [];
|
|
11412
|
+
foundDep = true;
|
|
11413
|
+
continue;
|
|
11414
|
+
}
|
|
11415
|
+
nonDepScans.push(scan);
|
|
10723
11416
|
}
|
|
10724
11417
|
const requirements = buildDependencyGraph(allDeps).map((dep) => {
|
|
10725
11418
|
const depId = `dependency-${dep.org}/${dep.name}`;
|
|
@@ -10760,12 +11453,27 @@ async function convertIonchannelToHdf(input, converterVersion = "1.0.0") {
|
|
|
10760
11453
|
status: "loaded"
|
|
10761
11454
|
});
|
|
10762
11455
|
baseline.maintainer = "saf@groups.mitre.org";
|
|
11456
|
+
baseline.description = verdictDescription(analysis);
|
|
11457
|
+
baseline.labels = verdictLabels(analysis);
|
|
11458
|
+
const baselines = [baseline];
|
|
11459
|
+
for (const scan of nonDepScans) {
|
|
11460
|
+
const scanBaseline = createMinimalBaseline(`Ion Channel ${scan.name} Scan`, [buildScanRequirement(scan)], {
|
|
11461
|
+
title: `Ion Channel Analysis of ${analysis.source}`,
|
|
11462
|
+
summary: scan.summary,
|
|
11463
|
+
integrity: {
|
|
11464
|
+
algorithm: resultsChecksum.algorithm,
|
|
11465
|
+
checksum: resultsChecksum.value
|
|
11466
|
+
},
|
|
11467
|
+
status: "loaded"
|
|
11468
|
+
});
|
|
11469
|
+
scanBaseline.maintainer = "saf@groups.mitre.org";
|
|
11470
|
+
baselines.push(scanBaseline);
|
|
11471
|
+
}
|
|
10763
11472
|
return buildHdfResults({
|
|
10764
11473
|
generatorName: "ionchannel-to-hdf",
|
|
10765
11474
|
converterVersion,
|
|
10766
11475
|
toolName: "Ion Channel",
|
|
10767
|
-
|
|
10768
|
-
baselines: [baseline],
|
|
11476
|
+
baselines,
|
|
10769
11477
|
timestamp: /* @__PURE__ */ new Date()
|
|
10770
11478
|
});
|
|
10771
11479
|
}
|
|
@@ -11738,7 +12446,8 @@ function buildDocument(amendments, converterVersion) {
|
|
|
11738
12446
|
...notes.length > 0 && { notes },
|
|
11739
12447
|
publisher: {
|
|
11740
12448
|
category: "other",
|
|
11741
|
-
name: publisherName
|
|
12449
|
+
name: publisherName,
|
|
12450
|
+
namespace: "https://saf.mitre.org"
|
|
11742
12451
|
},
|
|
11743
12452
|
tracking: {
|
|
11744
12453
|
id: trackingId,
|
|
@@ -12048,7 +12757,11 @@ function buildMetadata(a, docTime, converterVersion) {
|
|
|
12048
12757
|
}
|
|
12049
12758
|
async function buildSerialNumber(input, a) {
|
|
12050
12759
|
if (a.amendmentId) return `urn:uuid:${a.amendmentId}`;
|
|
12051
|
-
|
|
12760
|
+
const c = (await sha256(input)).slice(0, 32).split("");
|
|
12761
|
+
c[12] = "5";
|
|
12762
|
+
c[16] = (parseInt(c[16], 16) & 3 | 8).toString(16);
|
|
12763
|
+
const h = c.join("");
|
|
12764
|
+
return `urn:uuid:${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
|
|
12052
12765
|
}
|
|
12053
12766
|
//#endregion
|
|
12054
12767
|
//#region converters/oscal-to-hdf/typescript/detect.ts
|
|
@@ -13071,6 +13784,225 @@ function substituteParams(text, overrides) {
|
|
|
13071
13784
|
return result;
|
|
13072
13785
|
}
|
|
13073
13786
|
//#endregion
|
|
13074
|
-
|
|
13787
|
+
//#region shared/typescript/stix.ts
|
|
13788
|
+
/** Parse and validate a STIX 2.1 bundle. Objects are kept raw for lossless carriage. */
|
|
13789
|
+
function parseStixBundle(input) {
|
|
13790
|
+
if (!input) throw new Error("stix: empty input");
|
|
13791
|
+
let raw;
|
|
13792
|
+
try {
|
|
13793
|
+
raw = JSON.parse(input);
|
|
13794
|
+
} catch (e) {
|
|
13795
|
+
throw new Error(`stix: parsing bundle: ${e.message}`);
|
|
13796
|
+
}
|
|
13797
|
+
const obj = raw ?? {};
|
|
13798
|
+
if (obj.type !== "bundle") throw new Error(`stix: not a STIX bundle (type=${JSON.stringify(obj.type)})`);
|
|
13799
|
+
if (!Array.isArray(obj.objects)) throw new Error("stix: bundle has no objects[]");
|
|
13800
|
+
return {
|
|
13801
|
+
type: "bundle",
|
|
13802
|
+
id: typeof obj.id === "string" ? obj.id : void 0,
|
|
13803
|
+
objects: obj.objects.filter((o) => typeof o === "object" && o !== null && !Array.isArray(o))
|
|
13804
|
+
};
|
|
13805
|
+
}
|
|
13806
|
+
/** Report whether the input looks like a STIX 2.1 bundle, without throwing. */
|
|
13807
|
+
function detectStixBundle(input) {
|
|
13808
|
+
try {
|
|
13809
|
+
const o = JSON.parse(input);
|
|
13810
|
+
return o?.type === "bundle" && Array.isArray(o.objects);
|
|
13811
|
+
} catch {
|
|
13812
|
+
return false;
|
|
13813
|
+
}
|
|
13814
|
+
}
|
|
13815
|
+
/** A STIX object's id ('' if absent). */
|
|
13816
|
+
function stixObjectId(obj) {
|
|
13817
|
+
return typeof obj.id === "string" ? obj.id : "";
|
|
13818
|
+
}
|
|
13819
|
+
/**
|
|
13820
|
+
* The CVE ids a STIX object cites via external_references (source_name "cve").
|
|
13821
|
+
* A STIX vulnerability records its CVE this way, not in a native field
|
|
13822
|
+
* (STIX 2.1 §4.19).
|
|
13823
|
+
*/
|
|
13824
|
+
function stixObjectCVEs(obj) {
|
|
13825
|
+
const refs = obj.external_references;
|
|
13826
|
+
if (!Array.isArray(refs)) return [];
|
|
13827
|
+
const cves = [];
|
|
13828
|
+
for (const r of refs) {
|
|
13829
|
+
const ref = r;
|
|
13830
|
+
if (typeof ref.source_name === "string" && ref.source_name.toLowerCase() === "cve" && typeof ref.external_id === "string" && ref.external_id) cves.push(ref.external_id);
|
|
13831
|
+
}
|
|
13832
|
+
return cves;
|
|
13833
|
+
}
|
|
13834
|
+
//#endregion
|
|
13835
|
+
//#region shared/typescript/enrichStix.ts
|
|
13836
|
+
const DEFAULT_REVIEW_HORIZON_MS = 2160 * 60 * 60 * 1e3;
|
|
13837
|
+
/** Format a Date as canonical trimmed-UTC RFC3339 (seconds precision, matching Go's time.RFC3339). */
|
|
13838
|
+
function toRfc3339(d) {
|
|
13839
|
+
return d.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
13840
|
+
}
|
|
13841
|
+
/**
|
|
13842
|
+
* Overlay a STIX 2.1 bundle onto an existing HDF results document, attaching
|
|
13843
|
+
* each STIX object as an inert externalReferences[] entry: a CVE-bearing object
|
|
13844
|
+
* attaches to the finding whose requirementId is that CVE (fanning out to every
|
|
13845
|
+
* match), and everything else — non-CVE objects and CVEs with no matching
|
|
13846
|
+
* finding — attaches to the results root. Each entry carries the raw STIX object
|
|
13847
|
+
* losslessly in `document`.
|
|
13848
|
+
*
|
|
13849
|
+
* Informational only: authors no overrides and fabricates no status/impact (the
|
|
13850
|
+
* E:A recompute is a separate, opt-in step). The results document is preserved
|
|
13851
|
+
* verbatim except for the appended references — manipulated structurally so
|
|
13852
|
+
* every pre-existing field, including timestamp strings, round-trips unchanged.
|
|
13853
|
+
* TypeScript peer of shared/go/enrich_stix.go (kept at parity).
|
|
13854
|
+
*/
|
|
13855
|
+
function enrichStix(resultsInput, bundleInput, opts) {
|
|
13856
|
+
validateInputSize(resultsInput, "enrich-stix", opts?.maxSize);
|
|
13857
|
+
validateInputSize(bundleInput, "enrich-stix", opts?.maxSize);
|
|
13858
|
+
if (!resultsInput) throw new Error("enrich-stix: empty results input");
|
|
13859
|
+
const bundle = parseStixBundle(bundleInput);
|
|
13860
|
+
let parsed;
|
|
13861
|
+
try {
|
|
13862
|
+
parsed = JSON.parse(resultsInput);
|
|
13863
|
+
} catch (e) {
|
|
13864
|
+
throw new Error(`enrich-stix: parsing results: ${e.message}`);
|
|
13865
|
+
}
|
|
13866
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("enrich-stix: results input is not a JSON object");
|
|
13867
|
+
const doc = parsed;
|
|
13868
|
+
const reqById = indexRequirementsById(doc);
|
|
13869
|
+
for (const obj of bundle.objects) {
|
|
13870
|
+
let matched = false;
|
|
13871
|
+
for (const cve of stixObjectCVEs(obj)) for (const req of reqById.get(cve) ?? []) {
|
|
13872
|
+
appendExternalReference(req, buildStixRef(obj, "investigate"));
|
|
13873
|
+
matched = true;
|
|
13874
|
+
}
|
|
13875
|
+
if (!matched) appendExternalReference(doc, buildStixRef(obj, "reference"));
|
|
13876
|
+
}
|
|
13877
|
+
if (opts?.recomputeCvss) recomputeExploitation(bundle, reqById, opts);
|
|
13878
|
+
return JSON.stringify(doc, null, 2);
|
|
13879
|
+
}
|
|
13880
|
+
/**
|
|
13881
|
+
* Author an inline riskAdjustment on each CVE-matched finding that has a 3.1 base
|
|
13882
|
+
* vector and whose CVE carries a structural exploitation signal in the bundle.
|
|
13883
|
+
* Applies CVSS Exploit Maturity E:H (the 3.1 analog of 4.0 E:A) and recomputes
|
|
13884
|
+
* the Threat score. Skips (no fabrication) when a base vector is absent or non-3.1.
|
|
13885
|
+
* TypeScript peer of recomputeExploitation in enrich_stix.go.
|
|
13886
|
+
*/
|
|
13887
|
+
function recomputeExploitation(bundle, reqById, opts) {
|
|
13888
|
+
const appliedAt = opts.asOf ?? /* @__PURE__ */ new Date();
|
|
13889
|
+
const expiresAt = new Date(appliedAt.getTime() + (opts.reviewHorizonMs ?? DEFAULT_REVIEW_HORIZON_MS));
|
|
13890
|
+
for (const [cve, src] of exploitedCves(bundle)) for (const req of reqById.get(cve) ?? []) {
|
|
13891
|
+
const entry = findBaseVectorEntry(req, cve);
|
|
13892
|
+
if (!entry) continue;
|
|
13893
|
+
const baseVector = entry.baseVector;
|
|
13894
|
+
let score;
|
|
13895
|
+
try {
|
|
13896
|
+
score = computeCvssScore(`${baseVector}/E:H`);
|
|
13897
|
+
} catch {
|
|
13898
|
+
continue;
|
|
13899
|
+
}
|
|
13900
|
+
const ov = buildRiskAdjustment(cve, baseVector, score, src, appliedAt, expiresAt);
|
|
13901
|
+
req.statusOverrides = [...Array.isArray(req.statusOverrides) ? req.statusOverrides : [], ov];
|
|
13902
|
+
}
|
|
13903
|
+
}
|
|
13904
|
+
/**
|
|
13905
|
+
* Map each CVE with a structural exploitation signal in the bundle to the STIX
|
|
13906
|
+
* object providing it: a sighting (sighting_of_ref), a targets/exploits
|
|
13907
|
+
* relationship (target_ref), or an indicator/report (object_refs).
|
|
13908
|
+
*/
|
|
13909
|
+
function exploitedCves(bundle) {
|
|
13910
|
+
const vulnCves = /* @__PURE__ */ new Map();
|
|
13911
|
+
for (const o of bundle.objects) if (o.type === "vulnerability") {
|
|
13912
|
+
const id = stixObjectId(o);
|
|
13913
|
+
vulnCves.set(id, [...vulnCves.get(id) ?? [], ...stixObjectCVEs(o)]);
|
|
13914
|
+
}
|
|
13915
|
+
const exploited = /* @__PURE__ */ new Map();
|
|
13916
|
+
const mark = (vulnId, src) => {
|
|
13917
|
+
for (const cve of vulnCves.get(vulnId) ?? []) if (!exploited.has(cve)) exploited.set(cve, src);
|
|
13918
|
+
};
|
|
13919
|
+
for (const o of bundle.objects) if (o.type === "sighting" && typeof o.sighting_of_ref === "string") mark(o.sighting_of_ref, o);
|
|
13920
|
+
else if (o.type === "relationship" && (o.relationship_type === "targets" || o.relationship_type === "exploits") && typeof o.target_ref === "string") mark(o.target_ref, o);
|
|
13921
|
+
else if ((o.type === "indicator" || o.type === "report") && Array.isArray(o.object_refs)) {
|
|
13922
|
+
for (const r of o.object_refs) if (typeof r === "string") mark(r, o);
|
|
13923
|
+
}
|
|
13924
|
+
return exploited;
|
|
13925
|
+
}
|
|
13926
|
+
/** The finding's cvss[] entry carrying a base vector for the CVE (or a version-agnostic entry). */
|
|
13927
|
+
function findBaseVectorEntry(req, cve) {
|
|
13928
|
+
const arr = req.cvss;
|
|
13929
|
+
if (!Array.isArray(arr)) return void 0;
|
|
13930
|
+
for (const e of arr) {
|
|
13931
|
+
const m = e;
|
|
13932
|
+
const bv = m.baseVector;
|
|
13933
|
+
if (typeof bv === "string" && bv) {
|
|
13934
|
+
const id = m.id;
|
|
13935
|
+
if (id === void 0 || id === cve) return m;
|
|
13936
|
+
}
|
|
13937
|
+
}
|
|
13938
|
+
}
|
|
13939
|
+
/** Build the inline riskAdjustment recording the E:H-recomputed Threat score + STIX source. */
|
|
13940
|
+
function buildRiskAdjustment(cve, baseVector, score, src, appliedAt, expiresAt) {
|
|
13941
|
+
return {
|
|
13942
|
+
type: "riskAdjustment",
|
|
13943
|
+
reason: `${cve} actively exploited per STIX threat intelligence (${stixObjectId(src)}); CVSS Threat recomputed with Exploit Maturity E:H.`,
|
|
13944
|
+
impact: { value: roundImpact(score.temporalScore / 10) },
|
|
13945
|
+
appliedBy: {
|
|
13946
|
+
type: "other",
|
|
13947
|
+
identifier: "hdf-enrich"
|
|
13948
|
+
},
|
|
13949
|
+
appliedAt: toRfc3339(appliedAt),
|
|
13950
|
+
expiresAt: toRfc3339(expiresAt),
|
|
13951
|
+
cvss: {
|
|
13952
|
+
version: "3.1",
|
|
13953
|
+
id: cve,
|
|
13954
|
+
baseVector,
|
|
13955
|
+
baseScore: score.baseScore,
|
|
13956
|
+
threatVector: "E:H",
|
|
13957
|
+
threatScore: score.temporalScore,
|
|
13958
|
+
computedScore: score.temporalScore
|
|
13959
|
+
},
|
|
13960
|
+
externalReferences: [buildStixRef(src, "evidence")]
|
|
13961
|
+
};
|
|
13962
|
+
}
|
|
13963
|
+
/** Map each finding id to the requirement objects carrying it (fan-out across baselines). */
|
|
13964
|
+
function indexRequirementsById(doc) {
|
|
13965
|
+
const index = /* @__PURE__ */ new Map();
|
|
13966
|
+
const baselines = Array.isArray(doc.baselines) ? doc.baselines : [];
|
|
13967
|
+
for (const b of baselines) {
|
|
13968
|
+
const reqs = b?.requirements;
|
|
13969
|
+
if (!Array.isArray(reqs)) continue;
|
|
13970
|
+
for (const r of reqs) {
|
|
13971
|
+
const req = r;
|
|
13972
|
+
if (typeof req.id === "string" && req.id) {
|
|
13973
|
+
const list = index.get(req.id) ?? [];
|
|
13974
|
+
list.push(req);
|
|
13975
|
+
index.set(req.id, list);
|
|
13976
|
+
}
|
|
13977
|
+
}
|
|
13978
|
+
}
|
|
13979
|
+
return index;
|
|
13980
|
+
}
|
|
13981
|
+
/** Build an External_Reference envelope carrying the raw STIX object in `document`. `rel` is an open token. */
|
|
13982
|
+
function buildStixRef(obj, rel) {
|
|
13983
|
+
const ref = {
|
|
13984
|
+
sourceName: "stix",
|
|
13985
|
+
kind: "threat-intel",
|
|
13986
|
+
rel,
|
|
13987
|
+
document: obj
|
|
13988
|
+
};
|
|
13989
|
+
const id = stixObjectId(obj);
|
|
13990
|
+
if (id) ref.externalId = id;
|
|
13991
|
+
else ref.description = stixFallbackDescription(obj);
|
|
13992
|
+
return ref;
|
|
13993
|
+
}
|
|
13994
|
+
/** Human-readable description for an id-less STIX object: its name, else a
|
|
13995
|
+
* type-derived label, else a generic one — so its reference satisfies anyOf. */
|
|
13996
|
+
function stixFallbackDescription(obj) {
|
|
13997
|
+
if (typeof obj.name === "string" && obj.name) return obj.name;
|
|
13998
|
+
if (typeof obj.type === "string" && obj.type) return `STIX ${obj.type} object`;
|
|
13999
|
+
return "STIX object";
|
|
14000
|
+
}
|
|
14001
|
+
/** Append a reference to a container's externalReferences[], creating it if absent. */
|
|
14002
|
+
function appendExternalReference(container, ref) {
|
|
14003
|
+
container.externalReferences = [...Array.isArray(container.externalReferences) ? container.externalReferences : [], ref];
|
|
14004
|
+
}
|
|
14005
|
+
//#endregion
|
|
14006
|
+
export { convertAsffToHdf, convertAwsConfigToHdf, convertBurpsuiteToHdf, convertCheckovToHdf, convertCklToHdf, convertCklbToHdf, convertConveyorToHdf, convertCsafVexToHdf, convertCyclonedxToHdf, convertCyclonedxVexToHdf, convertDbprotectToHdf, convertDefectDojoToHdf, convertDeptrackToHdf, convertFortifyToHdf, convertGitlabToHdf, convertGosecToHdf, convertGrypeToHdf, convertHdfToAsff, convertHdfToCkl, convertHdfToCklb, convertHdfToCsafVex, convertHdfToCsv, convertHdfToCyclonedxVex, convertHdfToEcs, convertHdfToOcsf, convertHdfToOpenVex, convertHdfToOscalPoam, convertHdfToOscalSar, convertHdfToSplunk, convertHdfToXccdf, convertHdfToXml, convertHipcheckToHdf, convertIonchannelToHdf, convertJfrogXrayToHdf, convertJunitToHdf, convertMsftDefenderCloudToHdf, convertMsftDefenderDevopsToHdf, convertMsftDefenderEndpointToHdf, convertMsftSecureScoreToHdf, convertNessusToHdf, convertNetsparkerToHdf, convertNeuvectorToHdf, convertNiktoToHdf, convertOpenVexToHdf, convertOscalCatalogToHdf, convertOscalComponentToHdf, convertOscalPoamToHdf, convertOscalProfileToHdf, convertOscalSapToHdf, convertOscalSarToHdf, convertOscalSspToHdf, convertPrismaToHdf, convertSarifToHdf, convertScoutsuiteToHdf, convertSnykToHdf, convertSonarqubeToHdf, convertSplunkToHdf, convertTrufflehogToHdf, convertTwistlockToHdf, convertV1ToV2, convertVeracodeToHdf, convertXccdfResultsToHdf, convertZapToHdf, detectOscalDocumentType, detectStixBundle, enrichStix, isHDFV1, parseStixBundle };
|
|
13075
14007
|
|
|
13076
14008
|
//# sourceMappingURL=index.js.map
|