@mitre/hdf-converters 3.4.4 → 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 +52 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1309 -356
- 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,7 +734,47 @@ function convertProfile(v1Profile) {
|
|
|
753
734
|
if (v1Profile.depends?.length) v2Baseline.depends = v1Profile.depends.map(convertDependency);
|
|
754
735
|
return v2Baseline;
|
|
755
736
|
}
|
|
756
|
-
|
|
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
|
+
}
|
|
742
|
+
/**
|
|
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") {
|
|
757
778
|
validateInputSize(JSON.stringify(v1Data), "legacyhdf-to-hdf");
|
|
758
779
|
const v2 = {
|
|
759
780
|
baselines: (v1Data.profiles || []).map(convertProfile),
|
|
@@ -770,9 +791,13 @@ function convertV1ToV2(v1Data) {
|
|
|
770
791
|
}
|
|
771
792
|
v2.components = [component];
|
|
772
793
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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;
|
|
776
801
|
const knownV1Fields = /* @__PURE__ */ new Set([
|
|
777
802
|
"version",
|
|
778
803
|
"platform",
|
|
@@ -801,6 +826,63 @@ function isHDFV1(data) {
|
|
|
801
826
|
return typeof obj.version === "string" && Array.isArray(obj.profiles) && typeof obj.platform === "object" && obj.platform !== null;
|
|
802
827
|
}
|
|
803
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
|
|
804
886
|
//#region converters/asff-to-hdf/typescript/converter.ts
|
|
805
887
|
/**
|
|
806
888
|
* AWS Security Finding Format (ASFF) to HDF converter.
|
|
@@ -865,14 +947,17 @@ function severityLabelToImpact(label) {
|
|
|
865
947
|
/**
|
|
866
948
|
* findingImpact derives a 0.0–1.0 impact. Suppressed findings are forced to 0.
|
|
867
949
|
* Security Hub's INFORMATIONAL is up-graded to MEDIUM (Security Hub over-marks
|
|
868
|
-
* 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.
|
|
869
953
|
*/
|
|
870
954
|
function findingImpact(f) {
|
|
871
955
|
if (f.Workflow?.Status === "SUPPRESSED") return 0;
|
|
872
|
-
|
|
956
|
+
const sev = f.FindingProviderFields?.Severity?.Label ? f.FindingProviderFields.Severity : f.Severity;
|
|
957
|
+
let label = sev?.Label;
|
|
873
958
|
if (isSecurityHub(f) && (label ?? "").toUpperCase() === "INFORMATIONAL") label = "MEDIUM";
|
|
874
959
|
if (label) return severityLabelToImpact(label);
|
|
875
|
-
if (typeof
|
|
960
|
+
if (typeof sev?.Normalized === "number") return sev.Normalized / 100;
|
|
876
961
|
return 0;
|
|
877
962
|
}
|
|
878
963
|
function productArnParts(arn) {
|
|
@@ -1037,6 +1122,37 @@ function vulnerabilitySummary(f) {
|
|
|
1037
1122
|
return parts.join("; ");
|
|
1038
1123
|
}).join("\n");
|
|
1039
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
|
+
}
|
|
1040
1156
|
function trivyMessage(f) {
|
|
1041
1157
|
const o = f.Resources?.[0]?.Details?.Other;
|
|
1042
1158
|
if (!o) return "";
|
|
@@ -1076,10 +1192,15 @@ function buildRequirement$17(id, group) {
|
|
|
1076
1192
|
});
|
|
1077
1193
|
const nist = nistTags$1(group);
|
|
1078
1194
|
const tags = nist.length > 0 ? buildNistCciTags(nist, nistToCci(nist)) : {};
|
|
1195
|
+
const cves = vulnCves(primary);
|
|
1196
|
+
if (cves.length > 0) tags.cve = cves;
|
|
1079
1197
|
const results = group.map(buildResult$2);
|
|
1080
1198
|
const req = createRequirement(id, primary.Title ?? "", descriptions, impact, results, { tags });
|
|
1081
1199
|
const controlType = deriveControlTypeFromTags(nist);
|
|
1082
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);
|
|
1083
1204
|
const refs = [];
|
|
1084
1205
|
if (primary.SourceUrl) refs.push({ url: primary.SourceUrl });
|
|
1085
1206
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1135,10 +1256,7 @@ async function convertAsffToHdf(input, converterVersion = "1.0.0") {
|
|
|
1135
1256
|
name: "asff-to-hdf",
|
|
1136
1257
|
version: converterVersion
|
|
1137
1258
|
},
|
|
1138
|
-
tool: {
|
|
1139
|
-
name: "AWS Security Finding Format",
|
|
1140
|
-
format: "JSON"
|
|
1141
|
-
},
|
|
1259
|
+
tool: { name: "AWS Security Finding Format" },
|
|
1142
1260
|
timestamp: /* @__PURE__ */ new Date()
|
|
1143
1261
|
};
|
|
1144
1262
|
if (accounts.length > 0) {
|
|
@@ -1240,6 +1358,8 @@ function convertResultGroup(ruleId, rule, sarifResults) {
|
|
|
1240
1358
|
const controlType = deriveControlTypeFromTags(nistControls);
|
|
1241
1359
|
if (controlType !== void 0) req.controlType = controlType;
|
|
1242
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;
|
|
1243
1363
|
const seenKeys = /* @__PURE__ */ new Set();
|
|
1244
1364
|
const packages = [];
|
|
1245
1365
|
for (const sr of sarifResults) {
|
|
@@ -1494,7 +1614,6 @@ async function convertJunitToHdf(input, converterVersion = "1.0.0") {
|
|
|
1494
1614
|
generatorName: "junit-to-hdf",
|
|
1495
1615
|
converterVersion,
|
|
1496
1616
|
toolName: "JUnit XML",
|
|
1497
|
-
toolFormat: "XML",
|
|
1498
1617
|
baselines: [createMinimalBaseline(name, requirements, { resultsChecksum: await inputChecksum(input) })],
|
|
1499
1618
|
components: [{
|
|
1500
1619
|
type: TargetType.Application,
|
|
@@ -1546,7 +1665,7 @@ function testCaseToRequirement(tc, scanTime) {
|
|
|
1546
1665
|
const { status, message } = resolveStatus(tc);
|
|
1547
1666
|
const result = {
|
|
1548
1667
|
status,
|
|
1549
|
-
codeDesc: buildCodeDesc$
|
|
1668
|
+
codeDesc: buildCodeDesc$10(tc),
|
|
1550
1669
|
startTime: scanTime
|
|
1551
1670
|
};
|
|
1552
1671
|
if (message !== void 0) result.message = message;
|
|
@@ -1603,7 +1722,7 @@ function buildFailureMessage(message, typeName, body) {
|
|
|
1603
1722
|
if (body) result += "\n" + decodeXmlEntities$5(body);
|
|
1604
1723
|
return result;
|
|
1605
1724
|
}
|
|
1606
|
-
function buildCodeDesc$
|
|
1725
|
+
function buildCodeDesc$10(tc) {
|
|
1607
1726
|
if (tc.classname) return `${decodeXmlEntities$5(tc.classname)} :: ${decodeXmlEntities$5(tc.name)}`;
|
|
1608
1727
|
return decodeXmlEntities$5(tc.name);
|
|
1609
1728
|
}
|
|
@@ -1615,6 +1734,25 @@ function noFindingsTarget(baselineName, suites) {
|
|
|
1615
1734
|
//#endregion
|
|
1616
1735
|
//#region converters/xccdf-results-to-hdf/typescript/converter.ts
|
|
1617
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
|
+
/**
|
|
1618
1756
|
* Walk the Group tree depth-first, collecting every rule at any depth. Rules may
|
|
1619
1757
|
* sit directly on a Group that also has nested Groups.
|
|
1620
1758
|
*/
|
|
@@ -1859,6 +1997,12 @@ function ruleResultToRequirement(rr, ruleIndex, scanTime) {
|
|
|
1859
1997
|
label: "default",
|
|
1860
1998
|
data: stripHTML(extractVulnDiscussion(extractText(ruleDef?.description)))
|
|
1861
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
|
+
});
|
|
1862
2006
|
const fixtext = extractFixtext(ruleDef?.fixtext);
|
|
1863
2007
|
if (fixtext) descriptions.push({
|
|
1864
2008
|
label: "fix",
|
|
@@ -1875,6 +2019,10 @@ function ruleResultToRequirement(rr, ruleIndex, scanTime) {
|
|
|
1875
2019
|
const tags = buildCciNistTags(extractCCIs([...rr.ident ?? [], ...ruleDef?.ident ?? []]));
|
|
1876
2020
|
const nistTags = tags["nist"];
|
|
1877
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;
|
|
1878
2026
|
const controlType = deriveControlTypeFromTags(nistTags);
|
|
1879
2027
|
if (controlType !== void 0) req.controlType = controlType;
|
|
1880
2028
|
return req;
|
|
@@ -1923,6 +2071,35 @@ function decodeXmlEntities$4(s) {
|
|
|
1923
2071
|
return name !== void 0 ? NAMED_ENTITIES$4[name] ?? match : match;
|
|
1924
2072
|
});
|
|
1925
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
|
+
}
|
|
1926
2103
|
function extractText(field) {
|
|
1927
2104
|
if (field === void 0 || field === null) return "";
|
|
1928
2105
|
if (typeof field === "string") return decodeXmlEntities$4(field);
|
|
@@ -1945,6 +2122,67 @@ function extractFixtext(field) {
|
|
|
1945
2122
|
return decodeXmlEntities$4(field["#text"] ?? "");
|
|
1946
2123
|
}
|
|
1947
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
|
+
/**
|
|
1948
2186
|
* Extract the VulnDiscussion text from an XCCDF description that contains
|
|
1949
2187
|
* embedded XML like `<VulnDiscussion>...</VulnDiscussion>`. If no
|
|
1950
2188
|
* VulnDiscussion tag is found, returns the original text.
|
|
@@ -2759,6 +2997,19 @@ function synthesizePurl(ecosystem, name, version) {
|
|
|
2759
2997
|
if (ecosystem === Ecosystem.Generic) return void 0;
|
|
2760
2998
|
return `pkg:${ecosystem}/${name}@${version}`;
|
|
2761
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
|
+
}
|
|
2762
3013
|
function buildRequirement$16(vulnID, vulns, scanTime, packageManager) {
|
|
2763
3014
|
const rep = vulns[0];
|
|
2764
3015
|
const cweIDs = rep.identifiers.CWE ?? [];
|
|
@@ -2767,8 +3018,7 @@ function buildRequirement$16(vulnID, vulns, scanTime, packageManager) {
|
|
|
2767
3018
|
nist,
|
|
2768
3019
|
cci: nistToCci(nist)
|
|
2769
3020
|
};
|
|
2770
|
-
if (
|
|
2771
|
-
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;
|
|
2772
3022
|
if (rep.identifiers.GHSA && rep.identifiers.GHSA.length > 0) tags["ghsaid"] = rep.identifiers.GHSA;
|
|
2773
3023
|
const descriptions = [{
|
|
2774
3024
|
label: "default",
|
|
@@ -2786,6 +3036,9 @@ function buildRequirement$16(vulnID, vulns, scanTime, packageManager) {
|
|
|
2786
3036
|
const controlType = deriveControlTypeFromTags(nist);
|
|
2787
3037
|
if (controlType !== void 0) req.controlType = controlType;
|
|
2788
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;
|
|
2789
3042
|
const name = rep.packageName ?? rep.moduleName;
|
|
2790
3043
|
const version = rep.version;
|
|
2791
3044
|
if (name && version) {
|
|
@@ -2861,7 +3114,6 @@ async function convertSnykToHdf(input, converterVersion = "1.0.0") {
|
|
|
2861
3114
|
generatorName: "snyk-to-hdf",
|
|
2862
3115
|
converterVersion,
|
|
2863
3116
|
toolName: "Snyk",
|
|
2864
|
-
toolFormat: "JSON",
|
|
2865
3117
|
baselines,
|
|
2866
3118
|
components: [{
|
|
2867
3119
|
name: targetName,
|
|
@@ -2920,7 +3172,7 @@ function getReferences(vuln, relatedVulns) {
|
|
|
2920
3172
|
}
|
|
2921
3173
|
return Array.from(refs);
|
|
2922
3174
|
}
|
|
2923
|
-
function buildCodeDesc$
|
|
3175
|
+
function buildCodeDesc$9(match) {
|
|
2924
3176
|
const parts = [];
|
|
2925
3177
|
parts.push(`Package: ${match.artifact.name}@${match.artifact.version}`);
|
|
2926
3178
|
if (match.artifact.type) parts.push(`Type: ${match.artifact.type}`);
|
|
@@ -2934,35 +3186,16 @@ function buildCodeDesc$8(match) {
|
|
|
2934
3186
|
}
|
|
2935
3187
|
return parts.join(" | ");
|
|
2936
3188
|
}
|
|
2937
|
-
function
|
|
2938
|
-
switch (v) {
|
|
2939
|
-
case "2.0": return Version.The20;
|
|
2940
|
-
case "3.0": return Version.The30;
|
|
2941
|
-
case "4.0": return Version.The40;
|
|
2942
|
-
default: return Version.The31;
|
|
2943
|
-
}
|
|
2944
|
-
}
|
|
2945
|
-
function cvssBandSeverity(score) {
|
|
2946
|
-
switch (cvssScoreToSeverity(score)) {
|
|
2947
|
-
case "critical": return CVSSSeverity.Critical;
|
|
2948
|
-
case "high": return CVSSSeverity.High;
|
|
2949
|
-
case "medium": return CVSSSeverity.Medium;
|
|
2950
|
-
case "low": return CVSSSeverity.Low;
|
|
2951
|
-
default: return CVSSSeverity.None;
|
|
2952
|
-
}
|
|
2953
|
-
}
|
|
2954
|
-
function buildCvssEntries$1(vuln) {
|
|
3189
|
+
function buildCvssEntries$5(vuln) {
|
|
2955
3190
|
if (!vuln.cvss || vuln.cvss.length === 0) return;
|
|
2956
3191
|
const entries = [];
|
|
2957
3192
|
for (const c of vuln.cvss) {
|
|
2958
|
-
const entry = {
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
}
|
|
2965
|
-
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
|
+
});
|
|
2966
3199
|
if (entry.baseVector === void 0 && entry.baseScore === void 0) continue;
|
|
2967
3200
|
entries.push(entry);
|
|
2968
3201
|
}
|
|
@@ -2996,10 +3229,10 @@ function buildAffectedPackages(match) {
|
|
|
2996
3229
|
if (fix && fix.state === "fixed" && fix.versions && fix.versions.length > 0 && fix.versions[0]) pkg.fixedInVersion = fix.versions[0];
|
|
2997
3230
|
return [pkg];
|
|
2998
3231
|
}
|
|
2999
|
-
const CWE_ID_PATTERN = /^CWE-[1-9]\d*$/;
|
|
3232
|
+
const CWE_ID_PATTERN$1 = /^CWE-[1-9]\d*$/;
|
|
3000
3233
|
function extractCwe(raw) {
|
|
3001
3234
|
if (!raw || raw.length === 0) return void 0;
|
|
3002
|
-
const out = raw.filter((c) => CWE_ID_PATTERN.test(c));
|
|
3235
|
+
const out = raw.filter((c) => CWE_ID_PATTERN$1.test(c));
|
|
3003
3236
|
return out.length === 0 ? void 0 : out;
|
|
3004
3237
|
}
|
|
3005
3238
|
function buildEpss$2(entries) {
|
|
@@ -3041,7 +3274,7 @@ function convertMatchToRequirement(match, isIgnored, targetName, startTime) {
|
|
|
3041
3274
|
const message = messageParts.join(" ");
|
|
3042
3275
|
const result = {
|
|
3043
3276
|
status,
|
|
3044
|
-
codeDesc: buildCodeDesc$
|
|
3277
|
+
codeDesc: buildCodeDesc$9(match),
|
|
3045
3278
|
message,
|
|
3046
3279
|
startTime
|
|
3047
3280
|
};
|
|
@@ -3050,6 +3283,7 @@ function convertMatchToRequirement(match, isIgnored, targetName, startTime) {
|
|
|
3050
3283
|
id: isIgnored ? `Grype-Ignored-Match/${cveId}` : `Grype/${cveId}`,
|
|
3051
3284
|
title: `Grype found a vulnerability to ${cveId} in ${targetName}`,
|
|
3052
3285
|
impact,
|
|
3286
|
+
code: JSON.stringify(match, null, 2),
|
|
3053
3287
|
results: [result],
|
|
3054
3288
|
tags,
|
|
3055
3289
|
descriptions: [
|
|
@@ -3071,7 +3305,7 @@ function convertMatchToRequirement(match, isIgnored, targetName, startTime) {
|
|
|
3071
3305
|
};
|
|
3072
3306
|
const controlType = deriveControlTypeFromTags(DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
|
|
3073
3307
|
if (controlType !== void 0) requirement.controlType = controlType;
|
|
3074
|
-
const cvss = buildCvssEntries$
|
|
3308
|
+
const cvss = buildCvssEntries$5(vuln);
|
|
3075
3309
|
if (cvss) requirement.cvss = cvss;
|
|
3076
3310
|
requirement.affectedPackages = buildAffectedPackages(match);
|
|
3077
3311
|
const cwe = extractCwe(vuln.cwe);
|
|
@@ -3179,26 +3413,15 @@ function findingId(f) {
|
|
|
3179
3413
|
function cveList(f) {
|
|
3180
3414
|
return (f.vulnerability_ids ?? []).map((v) => v.vulnerability_id).filter(Boolean);
|
|
3181
3415
|
}
|
|
3182
|
-
function cvssBand(score) {
|
|
3183
|
-
switch (cvssScoreToSeverity(score)) {
|
|
3184
|
-
case "critical": return CVSSSeverity.Critical;
|
|
3185
|
-
case "high": return CVSSSeverity.High;
|
|
3186
|
-
case "medium": return CVSSSeverity.Medium;
|
|
3187
|
-
case "low": return CVSSSeverity.Low;
|
|
3188
|
-
default: return CVSSSeverity.None;
|
|
3189
|
-
}
|
|
3190
|
-
}
|
|
3191
3416
|
function buildCvss$1(f) {
|
|
3192
3417
|
const out = [];
|
|
3193
3418
|
const add = (version, vector, score) => {
|
|
3194
3419
|
if ((score === void 0 || score === null) && !vector) return;
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
}
|
|
3200
|
-
if (vector) entry.baseVector = vector;
|
|
3201
|
-
out.push(entry);
|
|
3420
|
+
out.push(buildCvss$2({
|
|
3421
|
+
version,
|
|
3422
|
+
baseScore: score,
|
|
3423
|
+
baseVector: vector
|
|
3424
|
+
}));
|
|
3202
3425
|
};
|
|
3203
3426
|
add(Version.The31, f.cvssv3, f.cvssv3_score);
|
|
3204
3427
|
add(Version.The40, f.cvssv4, f.cvssv4_score);
|
|
@@ -3241,6 +3464,8 @@ function codeDesc(f) {
|
|
|
3241
3464
|
function convertFinding(f) {
|
|
3242
3465
|
const nist = nistTags(f);
|
|
3243
3466
|
const tags = buildNistCciTags(nist, nistToCci(nist), triageTags(f));
|
|
3467
|
+
const cves = cveList(f);
|
|
3468
|
+
if (cves.length > 0) tags.cve = cves;
|
|
3244
3469
|
const result = {
|
|
3245
3470
|
status: deriveStatus(f),
|
|
3246
3471
|
codeDesc: codeDesc(f),
|
|
@@ -3260,6 +3485,7 @@ function convertFinding(f) {
|
|
|
3260
3485
|
const epss = buildEpss$1(f);
|
|
3261
3486
|
if (epss) req.epss = epss;
|
|
3262
3487
|
if (f.cwe && f.cwe > 0) req.cwe = [`CWE-${f.cwe}`];
|
|
3488
|
+
req.code = JSON.stringify(f, null, 2);
|
|
3263
3489
|
const firstRisk = f.accepted_risks?.[0];
|
|
3264
3490
|
if (f.risk_accepted && firstRisk) {
|
|
3265
3491
|
req.statusOverrides = [buildWaiverOverride(firstRisk)];
|
|
@@ -3468,7 +3694,7 @@ function convertReportItemToRequirement(item, host) {
|
|
|
3468
3694
|
if (controlType !== void 0) req.controlType = controlType;
|
|
3469
3695
|
if (verificationMethod !== void 0) req.verificationMethod = verificationMethod;
|
|
3470
3696
|
if (!isCompliance) {
|
|
3471
|
-
const cvssEntries = buildCvssEntries(item);
|
|
3697
|
+
const cvssEntries = buildCvssEntries$4(item);
|
|
3472
3698
|
if (cvssEntries.length > 0) req.cvss = cvssEntries;
|
|
3473
3699
|
const cweIDs = buildCweIDs(item);
|
|
3474
3700
|
if (cweIDs.length > 0) req.cwe = cweIDs;
|
|
@@ -3482,7 +3708,7 @@ function convertReportItemToRequirement(item, host) {
|
|
|
3482
3708
|
* the schema models cvss as a multi-entry array (Nessus emits one entry per
|
|
3483
3709
|
* item; multi-vendor convergence may yield more).
|
|
3484
3710
|
*/
|
|
3485
|
-
function buildCvssEntries(item) {
|
|
3711
|
+
function buildCvssEntries$4(item) {
|
|
3486
3712
|
const source = (item.cvss_score_source || "").trim();
|
|
3487
3713
|
if (!source || !CVE_SOURCE_RE.test(source)) return [];
|
|
3488
3714
|
const hasV3 = !!(item.cvss3_vector || item.cvss3_base_score);
|
|
@@ -3513,15 +3739,13 @@ function buildCvssEntries(item) {
|
|
|
3513
3739
|
if (baseVector) entry.baseVector = baseVector;
|
|
3514
3740
|
if (baseScore !== void 0) {
|
|
3515
3741
|
entry.baseScore = baseScore;
|
|
3516
|
-
|
|
3517
|
-
if (baseSeverity !== void 0) entry.baseSeverity = baseSeverity;
|
|
3742
|
+
entry.baseSeverity = cvssSeverityFromScore(baseScore);
|
|
3518
3743
|
}
|
|
3519
3744
|
if (threatVector !== void 0 && threatVector !== "") entry.threatVector = threatVector;
|
|
3520
3745
|
if (threatScore !== void 0) {
|
|
3521
3746
|
entry.threatScore = threatScore;
|
|
3522
3747
|
entry.computedScore = threatScore;
|
|
3523
|
-
|
|
3524
|
-
if (computedSeverity !== void 0) entry.computedSeverity = computedSeverity;
|
|
3748
|
+
entry.computedSeverity = cvssSeverityFromScore(threatScore);
|
|
3525
3749
|
}
|
|
3526
3750
|
return [entry];
|
|
3527
3751
|
}
|
|
@@ -3552,16 +3776,6 @@ function parseFloatOrUndef(s) {
|
|
|
3552
3776
|
const f = Number.parseFloat(s);
|
|
3553
3777
|
return Number.isFinite(f) ? f : void 0;
|
|
3554
3778
|
}
|
|
3555
|
-
function mapCvssSeverity(score) {
|
|
3556
|
-
switch (cvssScoreToSeverity(score)) {
|
|
3557
|
-
case "critical": return CVSSSeverity.Critical;
|
|
3558
|
-
case "high": return CVSSSeverity.High;
|
|
3559
|
-
case "medium": return CVSSSeverity.Medium;
|
|
3560
|
-
case "low": return CVSSSeverity.Low;
|
|
3561
|
-
case "none": return CVSSSeverity.None;
|
|
3562
|
-
default: return;
|
|
3563
|
-
}
|
|
3564
|
-
}
|
|
3565
3779
|
/**
|
|
3566
3780
|
* Extract CWE IDs from a ReportItem's <cwe> elements. Nessus emits bare
|
|
3567
3781
|
* numeric IDs (e.g. <cwe>200</cwe>); occasionally pipe-separated or prefixed
|
|
@@ -4048,7 +4262,7 @@ function buildCheckText(rule) {
|
|
|
4048
4262
|
}
|
|
4049
4263
|
return parts.join("<br/>");
|
|
4050
4264
|
}
|
|
4051
|
-
function buildCodeDesc$
|
|
4265
|
+
function buildCodeDesc$8(q) {
|
|
4052
4266
|
return `config_rule_name: ${q.ConfigRuleName}, resource_type: ${q.ResourceType}, resource_id: ${q.ResourceId}`;
|
|
4053
4267
|
}
|
|
4054
4268
|
function buildResultMessage(codeDesc, annotation, status) {
|
|
@@ -4065,7 +4279,7 @@ function computeRunTime(invoked, recorded) {
|
|
|
4065
4279
|
function buildResult(r) {
|
|
4066
4280
|
const q = r.EvaluationResultIdentifier.EvaluationResultQualifier;
|
|
4067
4281
|
const status = mapComplianceStatus(r.ComplianceType);
|
|
4068
|
-
const codeDesc = buildCodeDesc$
|
|
4282
|
+
const codeDesc = buildCodeDesc$8(q);
|
|
4069
4283
|
return createResult(status, buildResultMessage(codeDesc, r.Annotation, status), {
|
|
4070
4284
|
codeDesc,
|
|
4071
4285
|
startTime: (r.ConfigRuleInvokedTime ? parseTimestamp(r.ConfigRuleInvokedTime) : null) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z"),
|
|
@@ -4193,12 +4407,32 @@ function checkToResult(check, scanTime) {
|
|
|
4193
4407
|
});
|
|
4194
4408
|
}
|
|
4195
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
|
+
/**
|
|
4196
4427
|
* Converts a group of checks sharing a check_id into one EvaluatedRequirement.
|
|
4197
4428
|
*/
|
|
4198
|
-
function buildRequirement$14(checkId,
|
|
4429
|
+
function buildRequirement$14(checkId, group, scanTime) {
|
|
4430
|
+
const checks = group.map((c) => c.check);
|
|
4199
4431
|
const rep = checks[0];
|
|
4200
4432
|
const impact = getImpact$8(rep.severity);
|
|
4201
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;
|
|
4202
4436
|
const descriptions = [{
|
|
4203
4437
|
label: "default",
|
|
4204
4438
|
data: rep.check_name
|
|
@@ -4210,6 +4444,8 @@ function buildRequirement$14(checkId, checks, scanTime) {
|
|
|
4210
4444
|
const results = checks.map((check) => checkToResult(check, scanTime));
|
|
4211
4445
|
const req = createRequirement(checkId, rep.check_name, descriptions, impact, results, { tags });
|
|
4212
4446
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
4447
|
+
const code = renderCodeBlock(rep.code_block);
|
|
4448
|
+
if (code !== void 0) req.code = code;
|
|
4213
4449
|
const controlType = deriveControlTypeFromTags([...DEFAULT_STATIC_ANALYSIS_NIST_TAGS]);
|
|
4214
4450
|
if (controlType !== void 0) req.controlType = controlType;
|
|
4215
4451
|
return req;
|
|
@@ -4246,18 +4482,23 @@ async function convertCheckovToHdf(input, converterVersion = "1.0.0") {
|
|
|
4246
4482
|
for (const report of reports) {
|
|
4247
4483
|
checkTypes.push(report.check_type);
|
|
4248
4484
|
if (!version && report.summary.checkov_version) version = report.summary.checkov_version;
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
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
|
+
});
|
|
4252
4493
|
}
|
|
4253
4494
|
const { items: limitedChecks, truncated } = limitArray(allChecks);
|
|
4254
4495
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
4255
4496
|
if (truncated) console.warn(`WARNING: Input truncated at ${limitedChecks.length} check items (original: ${allChecks.length})`);
|
|
4256
4497
|
const groups = /* @__PURE__ */ new Map();
|
|
4257
|
-
for (const
|
|
4258
|
-
const existing = groups.get(check.check_id);
|
|
4259
|
-
if (existing) existing.push(
|
|
4260
|
-
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]);
|
|
4261
4502
|
}
|
|
4262
4503
|
const requirements = [];
|
|
4263
4504
|
for (const [checkId, checks] of groups) requirements.push(buildRequirement$14(checkId, checks, scanTime));
|
|
@@ -4272,7 +4513,6 @@ async function convertCheckovToHdf(input, converterVersion = "1.0.0") {
|
|
|
4272
4513
|
converterVersion,
|
|
4273
4514
|
toolName: "Checkov",
|
|
4274
4515
|
toolVersion: version,
|
|
4275
|
-
toolFormat: format,
|
|
4276
4516
|
baselines: [baseline],
|
|
4277
4517
|
timestamp: scanTime
|
|
4278
4518
|
});
|
|
@@ -4288,6 +4528,13 @@ const IMPACT_MAPPING$6 = {
|
|
|
4288
4528
|
LOW: .3
|
|
4289
4529
|
};
|
|
4290
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
|
+
/**
|
|
4291
4538
|
* Returns true if the issue is suppressed (via nosec flag or suppressions list).
|
|
4292
4539
|
*/
|
|
4293
4540
|
function isSuppressed(issue) {
|
|
@@ -4324,13 +4571,7 @@ function buildRequirement$13(ruleId, issues, scanTime) {
|
|
|
4324
4571
|
const rep = issues[0];
|
|
4325
4572
|
const impact = IMPACT_MAPPING$6[rep.severity.toUpperCase()] ?? .5;
|
|
4326
4573
|
const nist = mapCWEToNIST([rep.cwe.id], DEFAULT_REMEDIATION_NIST_TAGS$1);
|
|
4327
|
-
const tags = {
|
|
4328
|
-
nist,
|
|
4329
|
-
cwe: {
|
|
4330
|
-
id: rep.cwe.id,
|
|
4331
|
-
url: rep.cwe.url
|
|
4332
|
-
}
|
|
4333
|
-
};
|
|
4574
|
+
const tags = { nist };
|
|
4334
4575
|
const results = issues.map((issue) => issueToResult(issue, scanTime));
|
|
4335
4576
|
const descriptions = [{
|
|
4336
4577
|
label: "default",
|
|
@@ -4340,6 +4581,9 @@ function buildRequirement$13(ruleId, issues, scanTime) {
|
|
|
4340
4581
|
data: `CWE-${rep.cwe.id}: ${rep.cwe.url}`
|
|
4341
4582
|
}];
|
|
4342
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;
|
|
4343
4587
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
4344
4588
|
const controlType = deriveControlTypeFromTags(nist);
|
|
4345
4589
|
if (controlType !== void 0) req.controlType = controlType;
|
|
@@ -4392,7 +4636,7 @@ function buildNistTags$2(niktoId) {
|
|
|
4392
4636
|
if (control) return [control];
|
|
4393
4637
|
return [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS];
|
|
4394
4638
|
}
|
|
4395
|
-
function buildCodeDesc$
|
|
4639
|
+
function buildCodeDesc$7(vuln) {
|
|
4396
4640
|
const parts = [];
|
|
4397
4641
|
if (vuln.url) parts.push(`URL: ${vuln.url}`);
|
|
4398
4642
|
if (vuln.method) parts.push(`Method: ${vuln.method}`);
|
|
@@ -4403,7 +4647,7 @@ function convertVulnToRequirement(vuln) {
|
|
|
4403
4647
|
const cciTags = nistToCci(nistTags);
|
|
4404
4648
|
const result = {
|
|
4405
4649
|
status: ResultStatus.Failed,
|
|
4406
|
-
codeDesc: buildCodeDesc$
|
|
4650
|
+
codeDesc: buildCodeDesc$7(vuln),
|
|
4407
4651
|
startTime: /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
|
|
4408
4652
|
};
|
|
4409
4653
|
const extras = {};
|
|
@@ -4446,7 +4690,7 @@ async function convertNiktoToHdf(input, converterVersion = "unknown") {
|
|
|
4446
4690
|
const req = convertVulnToRequirement(primary);
|
|
4447
4691
|
if (vulns.length > 1) for (let i = 1; i < vulns.length; i++) req.results.push({
|
|
4448
4692
|
status: ResultStatus.Failed,
|
|
4449
|
-
codeDesc: buildCodeDesc$
|
|
4693
|
+
codeDesc: buildCodeDesc$7(vulns[i]),
|
|
4450
4694
|
startTime: /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
|
|
4451
4695
|
});
|
|
4452
4696
|
requirements.push(req);
|
|
@@ -4460,7 +4704,6 @@ async function convertNiktoToHdf(input, converterVersion = "unknown") {
|
|
|
4460
4704
|
generatorName: "nikto-to-hdf",
|
|
4461
4705
|
converterVersion,
|
|
4462
4706
|
toolName: "Nikto",
|
|
4463
|
-
toolFormat: "JSON",
|
|
4464
4707
|
baselines: [createMinimalBaseline(targetName, requirements, {
|
|
4465
4708
|
resultsChecksum,
|
|
4466
4709
|
title: `Nikto Target: ${targetName}`,
|
|
@@ -4493,7 +4736,13 @@ function buildNistTags$1(cweid) {
|
|
|
4493
4736
|
}
|
|
4494
4737
|
return [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS];
|
|
4495
4738
|
}
|
|
4496
|
-
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) {
|
|
4497
4746
|
const parts = [];
|
|
4498
4747
|
if (instance.uri) parts.push(`URI: ${instance.uri}`);
|
|
4499
4748
|
if (instance.method) parts.push(`Method: ${instance.method}`);
|
|
@@ -4501,6 +4750,21 @@ function buildCodeDesc$5(instance) {
|
|
|
4501
4750
|
if (instance.evidence) parts.push(`Evidence: ${instance.evidence}`);
|
|
4502
4751
|
return parts.join(" | ");
|
|
4503
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
|
+
}
|
|
4504
4768
|
function buildCheckDescription(alert) {
|
|
4505
4769
|
const parts = [];
|
|
4506
4770
|
if (alert.solution) {
|
|
@@ -4513,33 +4777,11 @@ function buildCheckDescription(alert) {
|
|
|
4513
4777
|
}
|
|
4514
4778
|
return parts.join("\n");
|
|
4515
4779
|
}
|
|
4516
|
-
function
|
|
4517
|
-
|
|
4518
|
-
let best = sites[0];
|
|
4519
|
-
for (let i = 1; i < sites.length; i++) {
|
|
4520
|
-
const site = sites[i];
|
|
4521
|
-
if ((site.alerts?.length ?? 0) > (best.alerts?.length ?? 0)) best = site;
|
|
4522
|
-
}
|
|
4523
|
-
return best;
|
|
4524
|
-
}
|
|
4525
|
-
const ZAP_RFC1123_LIKE = /^[A-Za-z]{3}, \d{1,2} [A-Za-z]{3} \d{4} \d{2}:\d{2}:\d{2}$/;
|
|
4526
|
-
function parseZapTimestamp(s) {
|
|
4527
|
-
const trimmed = s.trim();
|
|
4528
|
-
if (ZAP_RFC1123_LIKE.test(trimmed)) {
|
|
4529
|
-
const d = /* @__PURE__ */ new Date(`${trimmed} GMT`);
|
|
4530
|
-
if (!isNaN(d.getTime())) return d;
|
|
4531
|
-
}
|
|
4532
|
-
return parseTimestamp(s) ?? void 0;
|
|
4780
|
+
function siteLabel(site, index) {
|
|
4781
|
+
return site["@host"] || site["@name"] || `site ${index + 1}`;
|
|
4533
4782
|
}
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
registerAllFingerprints();
|
|
4537
|
-
const detected = detectConverter(input);
|
|
4538
|
-
if (detected && detected.fingerprint.id === "sarif-to-hdf") return convertSarifToHdf(input, converterVersion);
|
|
4539
|
-
const resultsChecksum = await inputChecksum(input);
|
|
4540
|
-
const zapData = parseJSON(input);
|
|
4541
|
-
const site = selectSite(Array.isArray(zapData.site) ? zapData.site : []);
|
|
4542
|
-
const alerts = site?.alerts ?? [];
|
|
4783
|
+
function buildSiteRequirements(site) {
|
|
4784
|
+
const alerts = site.alerts ?? [];
|
|
4543
4785
|
const pluginIdCount = /* @__PURE__ */ new Map();
|
|
4544
4786
|
const { items: limitedAlerts, truncated } = limitArray(alerts);
|
|
4545
4787
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
@@ -4552,7 +4794,6 @@ async function convertZapToHdf(input, converterVersion = "1.0.0") {
|
|
|
4552
4794
|
const nistTags = buildNistTags$1(alert.cweid ?? "");
|
|
4553
4795
|
const cciTags = nistToCci(nistTags);
|
|
4554
4796
|
const extras = {};
|
|
4555
|
-
if (alert.cweid) extras.cweid = alert.cweid;
|
|
4556
4797
|
if (alert.wascid) extras.wascid = alert.wascid;
|
|
4557
4798
|
if (alert.riskdesc) extras.riskdesc = alert.riskdesc;
|
|
4558
4799
|
if (alert.confidence) extras.confidence = alert.confidence;
|
|
@@ -4564,7 +4805,7 @@ async function convertZapToHdf(input, converterVersion = "1.0.0") {
|
|
|
4564
4805
|
for (const instance of limitedInstances) {
|
|
4565
4806
|
const result = {
|
|
4566
4807
|
status: ResultStatus.Failed,
|
|
4567
|
-
codeDesc: buildCodeDesc$
|
|
4808
|
+
codeDesc: buildCodeDesc$6(instance),
|
|
4568
4809
|
startTime: /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
|
|
4569
4810
|
};
|
|
4570
4811
|
if (instance.attack) result.message = instance.attack;
|
|
@@ -4591,40 +4832,76 @@ async function convertZapToHdf(input, converterVersion = "1.0.0") {
|
|
|
4591
4832
|
descriptions,
|
|
4592
4833
|
verificationMethod: VerificationMethodEnum.Automated
|
|
4593
4834
|
};
|
|
4835
|
+
const cwe = buildCwe(alert.cweid);
|
|
4836
|
+
if (cwe !== void 0) req.cwe = cwe;
|
|
4594
4837
|
const controlType = deriveControlTypeFromTags(nistTags);
|
|
4595
4838
|
if (controlType !== void 0) req.controlType = controlType;
|
|
4839
|
+
const code = buildRequirementCode$1(alert);
|
|
4840
|
+
if (code !== void 0) req.code = code;
|
|
4596
4841
|
requirements.push(req);
|
|
4597
4842
|
}
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
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;
|
|
4605
4851
|
}
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
const
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
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 = [];
|
|
4616
4865
|
const components = [];
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
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
|
+
});
|
|
4625
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"];
|
|
4626
4903
|
const hdf = {
|
|
4627
|
-
baselines
|
|
4904
|
+
baselines,
|
|
4628
4905
|
components,
|
|
4629
4906
|
generator: {
|
|
4630
4907
|
name: "zap-to-hdf",
|
|
@@ -5242,23 +5519,18 @@ function stringSlice(v) {
|
|
|
5242
5519
|
if (Array.isArray(v)) return v.filter((e) => typeof e === "string");
|
|
5243
5520
|
return [];
|
|
5244
5521
|
}
|
|
5245
|
-
/**
|
|
5522
|
+
/**
|
|
5523
|
+
* Most-significant status across a requirement's results[] (lossless), using
|
|
5524
|
+
* the canonical worst-wins ordering from @mitre/hdf-utilities.
|
|
5525
|
+
*/
|
|
5246
5526
|
function worstOfResults(req) {
|
|
5247
5527
|
const results = asArr(req.results) ?? [];
|
|
5248
|
-
const
|
|
5249
|
-
"failed",
|
|
5250
|
-
"error",
|
|
5251
|
-
"passed",
|
|
5252
|
-
"notReviewed",
|
|
5253
|
-
"notApplicable"
|
|
5254
|
-
];
|
|
5255
|
-
const present = /* @__PURE__ */ new Set();
|
|
5528
|
+
const statuses = [];
|
|
5256
5529
|
for (const rRaw of results) {
|
|
5257
5530
|
const r = asMap(rRaw);
|
|
5258
|
-
if (r)
|
|
5531
|
+
if (r) statuses.push(getStr(r, "status"));
|
|
5259
5532
|
}
|
|
5260
|
-
|
|
5261
|
-
return "notReviewed";
|
|
5533
|
+
return worstStatus(statuses);
|
|
5262
5534
|
}
|
|
5263
5535
|
/**
|
|
5264
5536
|
* Whether an HDF Result_Status is a failing verdict. Only 'failed' fails;
|
|
@@ -5514,10 +5786,34 @@ function maxImpact(ratings) {
|
|
|
5514
5786
|
return max;
|
|
5515
5787
|
}
|
|
5516
5788
|
/**
|
|
5517
|
-
*
|
|
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.
|
|
5518
5794
|
*/
|
|
5519
|
-
function
|
|
5520
|
-
|
|
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;
|
|
5521
5817
|
}
|
|
5522
5818
|
/**
|
|
5523
5819
|
* Formats a component reference as a code_desc string.
|
|
@@ -5585,8 +5881,8 @@ async function convertCyclonedxToHdf(input, converterVersion = "1.0.0") {
|
|
|
5585
5881
|
nist,
|
|
5586
5882
|
cci: nistToCci(nist)
|
|
5587
5883
|
};
|
|
5588
|
-
|
|
5589
|
-
|
|
5884
|
+
const cweIds = cwes.map((c) => `CWE-${c}`);
|
|
5885
|
+
const cvssEntries = buildCvssEntries$3(ratings);
|
|
5590
5886
|
const descriptions = [];
|
|
5591
5887
|
const defaultParts = [];
|
|
5592
5888
|
if (vuln.description) defaultParts.push(`Description: ${vuln.description}`);
|
|
@@ -5614,6 +5910,8 @@ async function convertCyclonedxToHdf(input, converterVersion = "1.0.0") {
|
|
|
5614
5910
|
const results = affects.length > 0 ? affects.map((affect) => toResult(formatCodeDesc$4(componentLookup, affect.ref))) : [toResult(`Vulnerability ${vuln.id}`)];
|
|
5615
5911
|
const title = vuln.source?.name ? `${vuln.id} (${vuln.source.name})` : vuln.id;
|
|
5616
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;
|
|
5617
5915
|
const controlType = deriveControlTypeFromTags(nist);
|
|
5618
5916
|
if (controlType !== void 0) req.controlType = controlType;
|
|
5619
5917
|
requirements.push(req);
|
|
@@ -5637,7 +5935,6 @@ async function convertCyclonedxToHdf(input, converterVersion = "1.0.0") {
|
|
|
5637
5935
|
generatorName: "cyclonedx-to-hdf",
|
|
5638
5936
|
converterVersion,
|
|
5639
5937
|
toolName: "CycloneDX",
|
|
5640
|
-
toolFormat: "JSON",
|
|
5641
5938
|
baselines: [baseline],
|
|
5642
5939
|
components: [{
|
|
5643
5940
|
...targetComponent,
|
|
@@ -6748,7 +7045,7 @@ function collectIdentifierTags(identifiers) {
|
|
|
6748
7045
|
}
|
|
6749
7046
|
return result;
|
|
6750
7047
|
}
|
|
6751
|
-
function buildCodeDesc$
|
|
7048
|
+
function buildCodeDesc$5(scanType, location) {
|
|
6752
7049
|
if (!location) return "";
|
|
6753
7050
|
switch (scanType) {
|
|
6754
7051
|
case "sast":
|
|
@@ -6826,7 +7123,7 @@ async function convertGitlabToHdf(input, converterVersion = "1.0.0") {
|
|
|
6826
7123
|
});
|
|
6827
7124
|
const result = {
|
|
6828
7125
|
status: ResultStatus.Failed,
|
|
6829
|
-
codeDesc: buildCodeDesc$
|
|
7126
|
+
codeDesc: buildCodeDesc$5(scanType, vuln.location),
|
|
6830
7127
|
startTime: startTime ? parseTimestamp(startTime) ?? /* @__PURE__ */ new Date("0001-01-01T00:00:00Z") : /* @__PURE__ */ new Date("0001-01-01T00:00:00Z")
|
|
6831
7128
|
};
|
|
6832
7129
|
const impact = gitlabSeverityToImpact(vuln.severity ?? "Unknown");
|
|
@@ -6834,6 +7131,7 @@ async function convertGitlabToHdf(input, converterVersion = "1.0.0") {
|
|
|
6834
7131
|
id: vuln.id,
|
|
6835
7132
|
title: vuln.name ?? vuln.id,
|
|
6836
7133
|
impact,
|
|
7134
|
+
code: JSON.stringify(vuln, null, 2),
|
|
6837
7135
|
results: [result],
|
|
6838
7136
|
tags,
|
|
6839
7137
|
descriptions,
|
|
@@ -6853,10 +7151,7 @@ async function convertGitlabToHdf(input, converterVersion = "1.0.0") {
|
|
|
6853
7151
|
title: `GitLab ${label} Security Scan`,
|
|
6854
7152
|
summary: `Scanner: ${scannerName}${scannerVersion ? ` v${scannerVersion}` : ""}`
|
|
6855
7153
|
});
|
|
6856
|
-
const tool = {
|
|
6857
|
-
name: scannerName,
|
|
6858
|
-
format: "JSON"
|
|
6859
|
-
};
|
|
7154
|
+
const tool = { name: scannerName };
|
|
6860
7155
|
if (scannerVersion) tool.version = scannerVersion;
|
|
6861
7156
|
const components = [];
|
|
6862
7157
|
const targetType = scanTypeToTargetType(scanType);
|
|
@@ -6889,6 +7184,20 @@ const TRUFFLEHOG_CCI = [
|
|
|
6889
7184
|
"CCI-002367"
|
|
6890
7185
|
];
|
|
6891
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
|
+
/**
|
|
6892
7201
|
* Parse TruffleHog input as JSON array, single object, or NDJSON.
|
|
6893
7202
|
*/
|
|
6894
7203
|
function parseFindings(input) {
|
|
@@ -6948,7 +7257,7 @@ function buildMessage(f) {
|
|
|
6948
7257
|
/**
|
|
6949
7258
|
* Build the Result.CodeDesc as JSON of SourceMetadata.
|
|
6950
7259
|
*/
|
|
6951
|
-
function buildCodeDesc$
|
|
7260
|
+
function buildCodeDesc$4(f) {
|
|
6952
7261
|
return JSON.stringify(f.SourceMetadata ?? {});
|
|
6953
7262
|
}
|
|
6954
7263
|
/**
|
|
@@ -6991,12 +7300,12 @@ function buildRequirement$12(reqID, findings) {
|
|
|
6991
7300
|
data: rep.DetectorDescription ?? `${rep.DetectorName} secret detected by ${rep.DecoderName} decoder`
|
|
6992
7301
|
}];
|
|
6993
7302
|
const results = findings.map((f) => createResult(ResultStatus.Failed, buildMessage(f), {
|
|
6994
|
-
codeDesc: buildCodeDesc$
|
|
7303
|
+
codeDesc: buildCodeDesc$4(f),
|
|
6995
7304
|
startTime: getTimestamp(f)
|
|
6996
7305
|
}));
|
|
6997
7306
|
const sourceFile = getSourceFile(rep);
|
|
6998
7307
|
const sourceLine = getSourceLine(rep);
|
|
6999
|
-
const req = createRequirement(reqID, title, descriptions,
|
|
7308
|
+
const req = createRequirement(reqID, title, descriptions, groupImpact(findings), results, {
|
|
7000
7309
|
tags,
|
|
7001
7310
|
sourceLocation: sourceFile ? {
|
|
7002
7311
|
ref: sourceFile,
|
|
@@ -7036,10 +7345,7 @@ async function convertTrufflehogToHdf(input, converterVersion = "1.0.0") {
|
|
|
7036
7345
|
name: "trufflehog-to-hdf",
|
|
7037
7346
|
version: converterVersion
|
|
7038
7347
|
},
|
|
7039
|
-
tool: {
|
|
7040
|
-
name: "TruffleHog",
|
|
7041
|
-
format: "JSON"
|
|
7042
|
-
},
|
|
7348
|
+
tool: { name: "TruffleHog" },
|
|
7043
7349
|
timestamp: /* @__PURE__ */ new Date()
|
|
7044
7350
|
};
|
|
7045
7351
|
const repoURL = findGitRepoURL(limitedFindings);
|
|
@@ -7173,7 +7479,6 @@ async function convertHipcheckToHdf(input, converterVersion = "1.0.0") {
|
|
|
7173
7479
|
generatorName: "hipcheck-to-hdf",
|
|
7174
7480
|
converterVersion,
|
|
7175
7481
|
toolName: "Hipcheck",
|
|
7176
|
-
toolFormat: "JSON",
|
|
7177
7482
|
toolVersion: report.hipcheck_version,
|
|
7178
7483
|
baselines: [baseline],
|
|
7179
7484
|
components: ident ? [{
|
|
@@ -7246,10 +7551,7 @@ async function convertBurpsuiteToHdf(input, converterVersion = "1.0.0") {
|
|
|
7246
7551
|
resultsChecksum,
|
|
7247
7552
|
title: `BurpSuite Scan: ${targetName}`
|
|
7248
7553
|
});
|
|
7249
|
-
const tool = {
|
|
7250
|
-
name: "BurpSuite",
|
|
7251
|
-
format: "XML"
|
|
7252
|
-
};
|
|
7554
|
+
const tool = { name: "BurpSuite" };
|
|
7253
7555
|
if (burpVersion) tool.version = burpVersion;
|
|
7254
7556
|
const hdf = {
|
|
7255
7557
|
baselines: [baseline],
|
|
@@ -7418,6 +7720,17 @@ function parseDate(dateStr) {
|
|
|
7418
7720
|
return parseTimestamp(month ? `${month[3]}-${MONTH_ABBR[month[1]] ?? "00"}-${month[2].padStart(2, "0")} ${month[4]}` : trimmed) ?? ZERO;
|
|
7419
7721
|
}
|
|
7420
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
|
+
/**
|
|
7421
7734
|
* Builds a single EvaluatedRequirement from a group of findings sharing a Check ID.
|
|
7422
7735
|
*/
|
|
7423
7736
|
function buildRequirement$10(checkID, findings, hasStatus) {
|
|
@@ -7436,6 +7749,7 @@ function buildRequirement$10(checkID, findings, hasStatus) {
|
|
|
7436
7749
|
});
|
|
7437
7750
|
const req = createRequirement(checkID, rep["Check"] ?? "", descriptions, getImpact$6(rep["Risk DV"] ?? ""), results, { tags });
|
|
7438
7751
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
7752
|
+
req.code = marshalFindingCode(rep);
|
|
7439
7753
|
const controlType = deriveControlTypeFromTags([...nist]);
|
|
7440
7754
|
if (controlType !== void 0) req.controlType = controlType;
|
|
7441
7755
|
return req;
|
|
@@ -7487,7 +7801,6 @@ async function convertDbprotectToHdf(input, converterVersion = "1.0.0") {
|
|
|
7487
7801
|
generatorName: "dbprotect-to-hdf",
|
|
7488
7802
|
converterVersion,
|
|
7489
7803
|
toolName: "DBProtect",
|
|
7490
|
-
toolFormat: "XML",
|
|
7491
7804
|
baselines: [baseline],
|
|
7492
7805
|
components: [{
|
|
7493
7806
|
name: targetName,
|
|
@@ -7531,30 +7844,6 @@ function buildSummary(result) {
|
|
|
7531
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"}`;
|
|
7532
7845
|
}
|
|
7533
7846
|
/**
|
|
7534
|
-
* Detects CVSS schema version enum from the vector prefix. Defaults to 3.1
|
|
7535
|
-
* since modern Twistlock exclusively emits 3.x output.
|
|
7536
|
-
*/
|
|
7537
|
-
function cvssVersionFromVector(vector) {
|
|
7538
|
-
if (!vector) return Version.The31;
|
|
7539
|
-
if (vector.startsWith("CVSS:2.0/")) return Version.The20;
|
|
7540
|
-
if (vector.startsWith("CVSS:3.0/")) return Version.The30;
|
|
7541
|
-
if (vector.startsWith("CVSS:4.0/")) return Version.The40;
|
|
7542
|
-
return Version.The31;
|
|
7543
|
-
}
|
|
7544
|
-
/**
|
|
7545
|
-
* Maps cvssScoreToSeverity('low'|'medium'|...) into the CVSSSeverity enum.
|
|
7546
|
-
*/
|
|
7547
|
-
function cvssSeverityFromScore(score) {
|
|
7548
|
-
switch (cvssScoreToSeverity(score)) {
|
|
7549
|
-
case "none": return CVSSSeverity.None;
|
|
7550
|
-
case "low": return CVSSSeverity.Low;
|
|
7551
|
-
case "medium": return CVSSSeverity.Medium;
|
|
7552
|
-
case "high": return CVSSSeverity.High;
|
|
7553
|
-
case "critical": return CVSSSeverity.Critical;
|
|
7554
|
-
default: return;
|
|
7555
|
-
}
|
|
7556
|
-
}
|
|
7557
|
-
/**
|
|
7558
7847
|
* Builds a Cvss entry from a Twistlock vulnerability. Returns undefined only
|
|
7559
7848
|
* when neither a score nor a vector is available. When the vendor emits a
|
|
7560
7849
|
* score but no vector (common in Twistlock/Prisma Cloud output), the Cvss
|
|
@@ -7565,16 +7854,12 @@ function buildCvss(vuln) {
|
|
|
7565
7854
|
const hasScore = typeof vuln.cvss === "number" && Number.isFinite(vuln.cvss);
|
|
7566
7855
|
const hasVector = !!vuln.vector;
|
|
7567
7856
|
if (!hasScore && !hasVector) return void 0;
|
|
7568
|
-
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
|
|
7572
|
-
|
|
7573
|
-
}
|
|
7574
|
-
if (hasVector) cv.baseVector = vuln.vector;
|
|
7575
|
-
const source = vuln.cve ?? vuln.id;
|
|
7576
|
-
if (source) cv.source = source;
|
|
7577
|
-
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
|
+
});
|
|
7578
7863
|
}
|
|
7579
7864
|
const CWE_REGEX = /cwe[-_]?(\d+)/gi;
|
|
7580
7865
|
/**
|
|
@@ -7764,7 +8049,6 @@ async function convertTwistlockToHdf(input, converterVersion = "1.0.0") {
|
|
|
7764
8049
|
generatorName: "twistlock-to-hdf",
|
|
7765
8050
|
converterVersion,
|
|
7766
8051
|
toolName: "Twistlock",
|
|
7767
|
-
toolFormat: "JSON",
|
|
7768
8052
|
baselines,
|
|
7769
8053
|
components: [component],
|
|
7770
8054
|
timestamp: /* @__PURE__ */ new Date()
|
|
@@ -7805,16 +8089,32 @@ function getCweIDs(cwes) {
|
|
|
7805
8089
|
return cwes.map((cwe) => `CWE-${cwe.cweId}`);
|
|
7806
8090
|
}
|
|
7807
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
|
+
/**
|
|
7808
8107
|
* Builds a single EvaluatedRequirement from a Dependency-Track finding.
|
|
7809
8108
|
*/
|
|
7810
8109
|
function buildRequirement$8(finding, timestamp) {
|
|
7811
8110
|
const cweIDs = getCweIDs(finding.vulnerability.cwes);
|
|
8111
|
+
const cveIDs = getCVEs(finding.vulnerability);
|
|
7812
8112
|
const nist = mapCWEToNIST(cweIDs, DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
|
|
7813
8113
|
const tags = {
|
|
7814
8114
|
nist,
|
|
7815
8115
|
cci: nistToCci(nist)
|
|
7816
8116
|
};
|
|
7817
|
-
if (
|
|
8117
|
+
if (cveIDs.length > 0) tags["cve"] = cveIDs;
|
|
7818
8118
|
const descriptions = [{
|
|
7819
8119
|
label: "default",
|
|
7820
8120
|
data: finding.vulnerability.description ?? ""
|
|
@@ -7834,8 +8134,10 @@ function buildRequirement$8(finding, timestamp) {
|
|
|
7834
8134
|
})];
|
|
7835
8135
|
const req = createRequirement(finding.matrix, getTitle$1(finding), descriptions, getImpact$5(finding.vulnerability.severity), results, { tags });
|
|
7836
8136
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
8137
|
+
req.code = JSON.stringify(finding, null, 2);
|
|
7837
8138
|
const controlType = deriveControlTypeFromTags(nist);
|
|
7838
8139
|
if (controlType !== void 0) req.controlType = controlType;
|
|
8140
|
+
if (cweIDs.length > 0) req.cwe = cweIDs;
|
|
7839
8141
|
const pkg = buildAffectedPackageFromComponent(finding.component);
|
|
7840
8142
|
if (pkg) req.affectedPackages = [pkg];
|
|
7841
8143
|
return req;
|
|
@@ -7887,7 +8189,7 @@ async function convertDeptrackToHdf(input, converterVersion = "1.0.0") {
|
|
|
7887
8189
|
generatorName: "deptrack-to-hdf",
|
|
7888
8190
|
converterVersion,
|
|
7889
8191
|
toolName: "Dependency-Track",
|
|
7890
|
-
toolFormat: "
|
|
8192
|
+
toolFormat: "FPF",
|
|
7891
8193
|
baselines: [baseline],
|
|
7892
8194
|
components: [{
|
|
7893
8195
|
name: targetName,
|
|
@@ -7906,12 +8208,79 @@ async function hashID(summary) {
|
|
|
7906
8208
|
return (await sha256(summary)).substring(0, 32);
|
|
7907
8209
|
}
|
|
7908
8210
|
/**
|
|
7909
|
-
*
|
|
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.
|
|
7910
8213
|
*/
|
|
7911
8214
|
function extractCWEs$1(entry) {
|
|
7912
|
-
const
|
|
7913
|
-
|
|
7914
|
-
|
|
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;
|
|
7915
8284
|
}
|
|
7916
8285
|
/**
|
|
7917
8286
|
* Build description from more_details, including CVE data.
|
|
@@ -7949,17 +8318,57 @@ function formatCodeDesc$1(entry) {
|
|
|
7949
8318
|
return parts.join("\n").replace(/,/g, ", ");
|
|
7950
8319
|
}
|
|
7951
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
|
+
/**
|
|
7952
8360
|
* Builds a single EvaluatedRequirement from a group of entries sharing an ID.
|
|
7953
8361
|
*/
|
|
7954
8362
|
function buildRequirement$7(entryID, entries, scanTime) {
|
|
7955
8363
|
const rep = entries[0];
|
|
7956
8364
|
const cweIDs = extractCWEs$1(rep);
|
|
8365
|
+
const cveIDs = extractCVEs$1(rep);
|
|
7957
8366
|
const nist = mapCWEToNIST(cweIDs, DEFAULT_STATIC_ANALYSIS_NIST_TAGS);
|
|
7958
8367
|
const tags = {
|
|
7959
8368
|
nist,
|
|
7960
8369
|
cci: nistToCci(nist)
|
|
7961
8370
|
};
|
|
7962
|
-
if (
|
|
8371
|
+
if (cveIDs.length > 0) tags["cve"] = cveIDs;
|
|
7963
8372
|
const descriptions = [{
|
|
7964
8373
|
label: "default",
|
|
7965
8374
|
data: formatDescription(rep)
|
|
@@ -7970,9 +8379,13 @@ function buildRequirement$7(entryID, entries, scanTime) {
|
|
|
7970
8379
|
startTime: scanTime
|
|
7971
8380
|
}));
|
|
7972
8381
|
const req = createRequirement(entryID, rep.summary, descriptions, severityToImpact$1(rep.severity), results, { tags });
|
|
8382
|
+
req.code = marshalEntryCode(rep);
|
|
7973
8383
|
const controlType = deriveControlTypeFromTags(nist);
|
|
7974
8384
|
if (controlType !== void 0) req.controlType = controlType;
|
|
7975
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;
|
|
7976
8389
|
const pkg = buildAffectedPackageFromEntry(rep);
|
|
7977
8390
|
if (pkg) req.affectedPackages = [pkg];
|
|
7978
8391
|
return req;
|
|
@@ -8051,7 +8464,6 @@ async function convertJfrogXrayToHdf(input, converterVersion = "1.0.0") {
|
|
|
8051
8464
|
generatorName: "jfrog-xray-to-hdf",
|
|
8052
8465
|
converterVersion,
|
|
8053
8466
|
toolName: "JFrog Xray",
|
|
8054
|
-
toolFormat: "JSON",
|
|
8055
8467
|
baselines: [createMinimalBaseline("JFrog Xray Scan", requirements, { resultsChecksum })],
|
|
8056
8468
|
components: [{
|
|
8057
8469
|
name: "JFrog Xray Scan",
|
|
@@ -8070,6 +8482,43 @@ function extractCWEs(description) {
|
|
|
8070
8482
|
return extractCWEIDs(description).map((id) => `CWE-${id}`);
|
|
8071
8483
|
}
|
|
8072
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
|
+
/**
|
|
8073
8522
|
* Computes the HDF impact from NeuVector CVSS scores.
|
|
8074
8523
|
* Prefers CVSS v3 score; falls back to CVSS v2 if v3 is 0.
|
|
8075
8524
|
* Impact is normalized to 0.0-1.0 by dividing by 10.
|
|
@@ -8098,29 +8547,66 @@ function vulnMessage(vuln) {
|
|
|
8098
8547
|
if (!vuln.fixed_version) return `Vulnerable package ${vuln.package_name} is at version ${vuln.package_version}. No fixed version available.`;
|
|
8099
8548
|
return `Vulnerable package ${vuln.package_name} is at version ${vuln.package_version}. Update to fixed version ${vuln.fixed_version}.`;
|
|
8100
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
|
+
}
|
|
8101
8582
|
/**
|
|
8102
8583
|
* Builds a single EvaluatedRequirement from a NeuVector vulnerability.
|
|
8103
8584
|
*/
|
|
8104
8585
|
function buildRequirement$6(vuln, scanTime) {
|
|
8105
8586
|
const cweIDs = extractCWEs(vuln.description);
|
|
8587
|
+
const cveIDs = extractCVEs(vuln);
|
|
8106
8588
|
const nist = mapCWEToNIST(cweIDs, DEFAULT_REMEDIATION_NIST_TAGS);
|
|
8107
8589
|
const tags = {
|
|
8108
8590
|
nist,
|
|
8109
8591
|
cci: nistToCci(nist)
|
|
8110
8592
|
};
|
|
8111
|
-
if (
|
|
8593
|
+
if (cveIDs.length > 0) tags["cve"] = cveIDs;
|
|
8112
8594
|
const descriptions = [{
|
|
8113
8595
|
label: "default",
|
|
8114
8596
|
data: vuln.description
|
|
8115
8597
|
}];
|
|
8116
8598
|
const results = [createResult(ResultStatus.Failed, vulnMessage(vuln), {
|
|
8117
|
-
codeDesc:
|
|
8599
|
+
codeDesc: buildCodeDesc$3(vuln),
|
|
8118
8600
|
startTime: scanTime
|
|
8119
8601
|
})];
|
|
8120
8602
|
const req = createRequirement(vulnID(vuln), vulnTitle(vuln), descriptions, getImpact$4(vuln), results, { tags });
|
|
8603
|
+
req.code = JSON.stringify(vuln, null, 2);
|
|
8121
8604
|
const controlType = deriveControlTypeFromTags(nist);
|
|
8122
8605
|
if (controlType !== void 0) req.controlType = controlType;
|
|
8123
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;
|
|
8124
8610
|
const cpe23 = (vuln.cpes ?? []).find((c) => /^cpe:2\.3:[aho]:/.test(c));
|
|
8125
8611
|
const pkg = buildAffectedPackage$1({
|
|
8126
8612
|
name: vuln.package_name,
|
|
@@ -8182,7 +8668,6 @@ async function convertNeuvectorToHdf(input, converterVersion = "1.0.0") {
|
|
|
8182
8668
|
generatorName: "neuvector-to-hdf",
|
|
8183
8669
|
converterVersion,
|
|
8184
8670
|
toolName: "NeuVector",
|
|
8185
|
-
toolFormat: "JSON",
|
|
8186
8671
|
baselines: [createMinimalBaseline("NeuVector Scan", requirements, {
|
|
8187
8672
|
resultsChecksum,
|
|
8188
8673
|
title: imageTitle(scan.report)
|
|
@@ -8201,7 +8686,9 @@ async function convertNeuvectorToHdf(input, converterVersion = "1.0.0") {
|
|
|
8201
8686
|
//#endregion
|
|
8202
8687
|
//#region converters/fortify-to-hdf/typescript/converter.ts
|
|
8203
8688
|
const NIST_REFERENCE_NAME = "Standards Mapping - NIST Special Publication 800-53 Revision 4";
|
|
8689
|
+
const CWE_REFERENCE_NAME = "Standards Mapping - Common Weakness Enumeration";
|
|
8204
8690
|
const NIST_PATTERN = /[a-zA-Z]{2}-\d+/g;
|
|
8691
|
+
const CWE_ID_PATTERN = /\d+/g;
|
|
8205
8692
|
const NAMED_ENTITIES$1 = {
|
|
8206
8693
|
lt: "<",
|
|
8207
8694
|
gt: ">",
|
|
@@ -8241,6 +8728,18 @@ function extractNISTFromReferences(refs) {
|
|
|
8241
8728
|
}
|
|
8242
8729
|
return [];
|
|
8243
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
|
+
}
|
|
8244
8743
|
function formatSnippet(snippet) {
|
|
8245
8744
|
const text = (snippet.Text ?? "").trim();
|
|
8246
8745
|
return `Path: ${snippet.File ?? ""}\nStartLine: ${snippet.StartLine ?? ""}, EndLine: ${snippet.EndLine ?? ""}\nCode:\n${text}`;
|
|
@@ -8263,8 +8762,26 @@ function buildCodeDesc$2(vuln, snippetMap) {
|
|
|
8263
8762
|
if (parts.length === 0) return `ClassID: ${vuln.ClassInfo?.ClassID ?? ""}, InstanceID: ${vuln.InstanceInfo?.InstanceID ?? ""}`;
|
|
8264
8763
|
return parts.join("\n");
|
|
8265
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
|
+
}
|
|
8266
8781
|
function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
|
|
8267
|
-
|
|
8782
|
+
const refs = ensureArray(desc.References?.Reference);
|
|
8783
|
+
const cweIDs = extractCWEFromReferences(refs);
|
|
8784
|
+
let nistTags = mergeCweNist(extractNISTFromReferences(refs), cweIDs);
|
|
8268
8785
|
if (nistTags.length === 0) nistTags = [...DEFAULT_STATIC_ANALYSIS_NIST_TAGS$1];
|
|
8269
8786
|
const cciTags = nistToCci(nistTags);
|
|
8270
8787
|
const tags = buildNistCciTags(nistTags, cciTags);
|
|
@@ -8280,7 +8797,7 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
|
|
|
8280
8797
|
data: stripFvdlMarkup(desc.Recommendations)
|
|
8281
8798
|
});
|
|
8282
8799
|
let impact = 0;
|
|
8283
|
-
if (vulns.length > 0) impact = parseFloat(vulns[0].
|
|
8800
|
+
if (vulns.length > 0) impact = parseFloat(vulns[0].InstanceInfo?.InstanceSeverity ?? "0") / 5;
|
|
8284
8801
|
const { items: limitedVulns, truncated } = limitArray(vulns);
|
|
8285
8802
|
/* v8 ignore next -- truncation only triggers with >100K items */
|
|
8286
8803
|
if (truncated) console.warn(`WARNING: Input truncated at ${limitedVulns.length} vulnerability items (original: ${vulns.length})`);
|
|
@@ -8298,8 +8815,11 @@ function buildRequirement$5(desc, vulns, snippetMap, startTimeStr) {
|
|
|
8298
8815
|
results,
|
|
8299
8816
|
verificationMethod: VerificationMethodEnum.Automated
|
|
8300
8817
|
};
|
|
8818
|
+
if (cweIDs.length > 0) req.cwe = cweIDs;
|
|
8301
8819
|
const controlType = deriveControlTypeFromTags(nistTags);
|
|
8302
8820
|
if (controlType !== void 0) req.controlType = controlType;
|
|
8821
|
+
const code = buildRequirementCode(vulns, snippetMap);
|
|
8822
|
+
if (code !== void 0) req.code = code;
|
|
8303
8823
|
return req;
|
|
8304
8824
|
}
|
|
8305
8825
|
/**
|
|
@@ -8456,6 +8976,8 @@ function buildRequirement$4(rec, scanTime) {
|
|
|
8456
8976
|
const controlType = deriveControlTypeFromTags(nist);
|
|
8457
8977
|
if (controlType !== void 0) req.controlType = controlType;
|
|
8458
8978
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
8979
|
+
const cvss = buildCvssEntries(rec);
|
|
8980
|
+
if (cvss) req.cvss = cvss;
|
|
8459
8981
|
if (rec["CVE ID"]) {
|
|
8460
8982
|
const pkg = buildAffectedPackageFromRecord(rec);
|
|
8461
8983
|
if (pkg) req.affectedPackages = [pkg];
|
|
@@ -8463,6 +8985,32 @@ function buildRequirement$4(rec, scanTime) {
|
|
|
8463
8985
|
return req;
|
|
8464
8986
|
}
|
|
8465
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
|
+
/**
|
|
8466
9014
|
* Distro slugs in Prisma look like `redhat-RHEL7`, `debian-buster`,
|
|
8467
9015
|
* `alpine-3.14`, `ubuntu-20.04`. Only the leading vendor segment is
|
|
8468
9016
|
* mapped — unknown vendors fall back to `generic` rather than guessing.
|
|
@@ -8555,7 +9103,6 @@ async function convertPrismaToHdf(input, converterVersion = "1.0.0") {
|
|
|
8555
9103
|
generatorName: "prisma-to-hdf",
|
|
8556
9104
|
converterVersion,
|
|
8557
9105
|
toolName: "Prisma Cloud",
|
|
8558
|
-
toolFormat: "CSV",
|
|
8559
9106
|
baselines,
|
|
8560
9107
|
components,
|
|
8561
9108
|
timestamp: scanTime
|
|
@@ -8612,6 +9159,36 @@ function formatControlDesc(vuln) {
|
|
|
8612
9159
|
return parts.join("\n");
|
|
8613
9160
|
}
|
|
8614
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
|
+
/**
|
|
8615
9192
|
* Performs dual NIST mapping from both CWE and OWASP IDs.
|
|
8616
9193
|
* Returns a deduplicated sorted list of NIST controls, falling back to
|
|
8617
9194
|
* DEFAULT_STATIC_ANALYSIS_NIST_TAGS if no mappings are found.
|
|
@@ -8683,6 +9260,10 @@ function buildRequirement$3(vuln, initiated) {
|
|
|
8683
9260
|
const controlType = deriveControlTypeFromTags(nist);
|
|
8684
9261
|
if (controlType !== void 0) req.controlType = controlType;
|
|
8685
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;
|
|
8686
9267
|
return req;
|
|
8687
9268
|
}
|
|
8688
9269
|
/**
|
|
@@ -8696,7 +9277,7 @@ async function convertNetsparkerToHdf(input, converterVersion = "1.0.0") {
|
|
|
8696
9277
|
if (!input || input.trim().length === 0) throw new Error("netsparker: empty input");
|
|
8697
9278
|
validateInputSize(input, "netsparker");
|
|
8698
9279
|
const resultsChecksum = await inputChecksum(input);
|
|
8699
|
-
const parsed = parseXmlWithArrays(input, ["vulnerability"]);
|
|
9280
|
+
const parsed = parseXmlWithArrays(input, ["vulnerability", "score"]);
|
|
8700
9281
|
const isInvicti = !!parsed["invicti-enterprise"];
|
|
8701
9282
|
const data = parsed["invicti-enterprise"] ?? parsed["netsparker-enterprise"] ?? {};
|
|
8702
9283
|
if (!data.vulnerabilities && !data.target) throw new Error("netsparker: invalid XML — missing expected root element");
|
|
@@ -8717,7 +9298,6 @@ async function convertNetsparkerToHdf(input, converterVersion = "1.0.0") {
|
|
|
8717
9298
|
generatorName: "netsparker-to-hdf",
|
|
8718
9299
|
converterVersion,
|
|
8719
9300
|
toolName,
|
|
8720
|
-
toolFormat: "XML",
|
|
8721
9301
|
baselines: [createMinimalBaseline("Netsparker Scan", requirements, {
|
|
8722
9302
|
resultsChecksum,
|
|
8723
9303
|
title: `${toolName} Enterprise Scan ID: ${target["scan-id"] ?? ""} URL: ${target.url ?? ""}`
|
|
@@ -8851,7 +9431,6 @@ async function convertScoutsuiteToHdf(input, converterVersion = "1.0.0") {
|
|
|
8851
9431
|
generatorName: "scoutsuite-to-hdf",
|
|
8852
9432
|
converterVersion,
|
|
8853
9433
|
toolName: "ScoutSuite",
|
|
8854
|
-
toolFormat: "JSON",
|
|
8855
9434
|
toolVersion: report.last_run.version,
|
|
8856
9435
|
baselines: [baseline],
|
|
8857
9436
|
components: [{
|
|
@@ -9015,7 +9594,6 @@ async function convertConveyorToHdf(input, converterVersion = "1.0.0") {
|
|
|
9015
9594
|
generatorName: "conveyor-to-hdf",
|
|
9016
9595
|
converterVersion,
|
|
9017
9596
|
toolName: "Conveyor",
|
|
9018
|
-
toolFormat: "JSON",
|
|
9019
9597
|
baselines,
|
|
9020
9598
|
components: [{
|
|
9021
9599
|
name: targetName,
|
|
@@ -9097,23 +9675,6 @@ function formatRecommendations(rec) {
|
|
|
9097
9675
|
}
|
|
9098
9676
|
return parts.join("\n");
|
|
9099
9677
|
}
|
|
9100
|
-
/** Format CWE data for the cweid tag. */
|
|
9101
|
-
function formatCWEData(cwes) {
|
|
9102
|
-
return cwes.map((c) => {
|
|
9103
|
-
let entry = `CWE-${attr(c, "cweid")}: ${attr(c, "cwename")}`;
|
|
9104
|
-
const categories = [
|
|
9105
|
-
["pcrirelated", attr(c, "pcirelated")],
|
|
9106
|
-
["owasp", attr(c, "owasp")],
|
|
9107
|
-
["sans", attr(c, "sans")],
|
|
9108
|
-
["certc", attr(c, "certc")],
|
|
9109
|
-
["certcpp", attr(c, "certcpp")],
|
|
9110
|
-
["certjava", attr(c, "certjava")],
|
|
9111
|
-
["owaspmobile", attr(c, "owaspmobile")]
|
|
9112
|
-
];
|
|
9113
|
-
for (const [name, val] of categories) if (val) entry += `${name}: ${val}\n`;
|
|
9114
|
-
return entry;
|
|
9115
|
-
}).join("\n");
|
|
9116
|
-
}
|
|
9117
9678
|
/** Format CWE descriptions for the cweDescription tag. */
|
|
9118
9679
|
function formatCWEDesc(cwes) {
|
|
9119
9680
|
return cwes.map((c) => {
|
|
@@ -9174,6 +9735,54 @@ function formatSCACodeDesc(comp) {
|
|
|
9174
9735
|
}
|
|
9175
9736
|
return parts.join("\n");
|
|
9176
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
|
+
}
|
|
9177
9786
|
/** Build CWE-based requirements from severity categories. */
|
|
9178
9787
|
function buildCWERequirements(severities, firstBuildDate) {
|
|
9179
9788
|
const requirements = [];
|
|
@@ -9187,14 +9796,14 @@ function buildCWERequirements(severities, firstBuildDate) {
|
|
|
9187
9796
|
/** Build a single CWE-based requirement from a category. */
|
|
9188
9797
|
function buildCWERequirement(cat, impact, firstBuildDate) {
|
|
9189
9798
|
const cwes = ensureArray(cat.cwe);
|
|
9190
|
-
const
|
|
9799
|
+
const cweIDs = cwes.map((c) => attr(c, "cweid")).filter(Boolean);
|
|
9800
|
+
const nist = mapCWEToNIST(cweIDs, DEFAULT_REMEDIATION_NIST_TAGS$1);
|
|
9191
9801
|
const cciTags = nistToCci(nist);
|
|
9192
9802
|
const extras = {};
|
|
9193
|
-
const cweData = formatCWEData(cwes);
|
|
9194
|
-
if (cweData) extras.cweid = cweData;
|
|
9195
9803
|
const cweDescStr = formatCWEDesc(cwes);
|
|
9196
9804
|
if (cweDescStr) extras.cweDescription = cweDescStr;
|
|
9197
9805
|
const tags = buildNistCciTags(nist, cciTags, extras);
|
|
9806
|
+
const cweList = cweIDs.map((id) => `CWE-${id}`);
|
|
9198
9807
|
const descriptions = [{
|
|
9199
9808
|
label: "default",
|
|
9200
9809
|
data: formatDesc(cat.desc)
|
|
@@ -9216,6 +9825,10 @@ function buildCWERequirement(cat, impact, firstBuildDate) {
|
|
|
9216
9825
|
const staticflaws = c.staticflaws;
|
|
9217
9826
|
return ensureArray(staticflaws?.flaw).map((flaw) => attr(flaw, "sourcefile")).filter(Boolean);
|
|
9218
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
|
+
});
|
|
9219
9832
|
const req = createRequirement(attr(cat, "categoryid"), attr(cat, "categoryname"), descriptions, impact, results, {
|
|
9220
9833
|
tags,
|
|
9221
9834
|
sourceLocation: sourceRef ? { ref: sourceRef } : void 0
|
|
@@ -9223,6 +9836,8 @@ function buildCWERequirement(cat, impact, firstBuildDate) {
|
|
|
9223
9836
|
const controlType = deriveControlTypeFromTags(nist);
|
|
9224
9837
|
if (controlType !== void 0) req.controlType = controlType;
|
|
9225
9838
|
req.verificationMethod = VerificationMethodEnum.Automated;
|
|
9839
|
+
if (cweList.length > 0) req.cwe = cweList;
|
|
9840
|
+
if (codeLines.length > 0) req.code = codeLines.join("\n");
|
|
9226
9841
|
return req;
|
|
9227
9842
|
}
|
|
9228
9843
|
/** Build CVE-based requirements from SCA components. */
|
|
@@ -9257,6 +9872,30 @@ function buildCVERequirements(sca, firstBuildDate) {
|
|
|
9257
9872
|
return buildCVERequirement(entry.vuln, entry.components, firstBuildDate);
|
|
9258
9873
|
});
|
|
9259
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
|
+
}
|
|
9260
9899
|
/** Build a single CVE-based requirement. */
|
|
9261
9900
|
function buildCVERequirement(vuln, components, firstBuildDate) {
|
|
9262
9901
|
const impact = veracodeSeverityToImpact(attr(vuln, "severity"));
|
|
@@ -9265,9 +9904,7 @@ function buildCVERequirement(vuln, components, firstBuildDate) {
|
|
|
9265
9904
|
if (cweID) nist = mapCWEToNIST([cweID.replace(/^CWE-/, "")], DEFAULT_REMEDIATION_NIST_TAGS$1);
|
|
9266
9905
|
else nist = DEFAULT_REMEDIATION_NIST_TAGS$1;
|
|
9267
9906
|
const cciTags = nistToCci(nist);
|
|
9268
|
-
const
|
|
9269
|
-
if (cweID) extras.cwe = cweID;
|
|
9270
|
-
const tags = buildNistCciTags(nist, cciTags, extras);
|
|
9907
|
+
const tags = buildNistCciTags(nist, cciTags, {});
|
|
9271
9908
|
const startTime = parseVeracodeTimestamp(firstBuildDate) ?? /* @__PURE__ */ new Date();
|
|
9272
9909
|
const results = components.map((comp) => createResult(ResultStatus.Failed, void 0, {
|
|
9273
9910
|
codeDesc: formatSCACodeDesc(comp),
|
|
@@ -9291,6 +9928,10 @@ function buildCVERequirement(vuln, components, firstBuildDate) {
|
|
|
9291
9928
|
const controlType = deriveControlTypeFromTags(nist);
|
|
9292
9929
|
if (controlType !== void 0) req.controlType = controlType;
|
|
9293
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);
|
|
9294
9935
|
return req;
|
|
9295
9936
|
}
|
|
9296
9937
|
/**
|
|
@@ -9336,7 +9977,6 @@ async function convertVeracodeToHdf(input, converterVersion = "1.0.0") {
|
|
|
9336
9977
|
generatorName: "veracode-to-hdf",
|
|
9337
9978
|
converterVersion,
|
|
9338
9979
|
toolName: "Veracode",
|
|
9339
|
-
toolFormat: "XML",
|
|
9340
9980
|
baselines: [baseline],
|
|
9341
9981
|
components: [{
|
|
9342
9982
|
name: targetName,
|
|
@@ -9444,7 +10084,6 @@ async function convertMsftSecureScoreToHdf(input, converterVersion = "1.0.0") {
|
|
|
9444
10084
|
generatorName: "msft-secure-score-to-hdf",
|
|
9445
10085
|
converterVersion,
|
|
9446
10086
|
toolName: "Microsoft Secure Score",
|
|
9447
|
-
toolFormat: "JSON",
|
|
9448
10087
|
baselines: combined.secureScore.value.map((ss) => {
|
|
9449
10088
|
if (!tenantId) tenantId = ss.azureTenantId;
|
|
9450
10089
|
const { items: limitedControlScores, truncated } = limitArray(ss.controlScores);
|
|
@@ -9680,7 +10319,6 @@ async function convertMsftDefenderCloudToHdf(input, converterVersion = "1.0.0")
|
|
|
9680
10319
|
generatorName: "msft-defender-cloud-to-hdf",
|
|
9681
10320
|
converterVersion,
|
|
9682
10321
|
toolName: "Microsoft Defender for Cloud",
|
|
9683
|
-
toolFormat: "JSON",
|
|
9684
10322
|
baselines: [baseline],
|
|
9685
10323
|
components,
|
|
9686
10324
|
timestamp: scanTime
|
|
@@ -9956,8 +10594,6 @@ function buildRuleObj(req) {
|
|
|
9956
10594
|
}
|
|
9957
10595
|
const rationale = findDescription(req.descriptions, "rationale");
|
|
9958
10596
|
if (rationale) rule.rationale = wrap(rationale);
|
|
9959
|
-
const fixtext = findDescription(req.descriptions, "fix");
|
|
9960
|
-
if (fixtext) rule.fixtext = wrap(fixtext);
|
|
9961
10597
|
const idents = [];
|
|
9962
10598
|
if (req.tags && Array.isArray(req.tags["cci"])) for (const cci of req.tags["cci"]) idents.push({
|
|
9963
10599
|
[`${ATTR}system`]: "http://cyber.mil/cci",
|
|
@@ -9968,6 +10604,8 @@ function buildRuleObj(req) {
|
|
|
9968
10604
|
"#text": n
|
|
9969
10605
|
});
|
|
9970
10606
|
if (idents.length > 0) rule.ident = idents;
|
|
10607
|
+
const fixtext = findDescription(req.descriptions, "fix");
|
|
10608
|
+
if (fixtext) rule.fixtext = wrap(fixtext);
|
|
9971
10609
|
const checks = [];
|
|
9972
10610
|
const checkContent = findDescription(req.descriptions, "check");
|
|
9973
10611
|
if (checkContent) checks.push({
|
|
@@ -9998,12 +10636,19 @@ function buildTestResultObj(hdfData, baseline) {
|
|
|
9998
10636
|
if (target.ipAddress) testResult["target-address"] = wrap(target.ipAddress);
|
|
9999
10637
|
} else testResult.target = wrap("unknown");
|
|
10000
10638
|
const ruleResults = [];
|
|
10639
|
+
let passed = 0;
|
|
10640
|
+
let scorable = 0;
|
|
10001
10641
|
for (const req of baseline.requirements) {
|
|
10002
10642
|
const ruleIdRef = sanitizeXccdfId("xccdf_hdf_rule_" + req.id + "_rule");
|
|
10003
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++;
|
|
10004
10649
|
const rr = {
|
|
10005
10650
|
[`${ATTR}idref`]: ruleIdRef,
|
|
10006
|
-
result: wrap(
|
|
10651
|
+
result: wrap(status)
|
|
10007
10652
|
};
|
|
10008
10653
|
rr[`${ATTR}time`] = typeof result.startTime === "string" ? result.startTime : result.startTime.toISOString();
|
|
10009
10654
|
if (result.message) rr.message = wrap(result.message);
|
|
@@ -10015,6 +10660,12 @@ function buildTestResultObj(hdfData, baseline) {
|
|
|
10015
10660
|
}
|
|
10016
10661
|
}
|
|
10017
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
|
+
};
|
|
10018
10669
|
return testResult;
|
|
10019
10670
|
}
|
|
10020
10671
|
//#endregion
|
|
@@ -10207,23 +10858,40 @@ async function convertHdfToOscalSar(input) {
|
|
|
10207
10858
|
return JSON.stringify(doc, null, 2);
|
|
10208
10859
|
}
|
|
10209
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
|
+
/**
|
|
10210
10872
|
* Constructs the full OSCAL assessment-results document from HDF results.
|
|
10211
10873
|
*/
|
|
10212
10874
|
function buildOSCALDocument(hdfResults) {
|
|
10213
10875
|
let timestamp = formatTimestampSeconds(/* @__PURE__ */ new Date());
|
|
10214
10876
|
const documentTime = hdfTime(hdfResults.timestamp);
|
|
10215
10877
|
if (documentTime) timestamp = formatTimestampSeconds(documentTime);
|
|
10878
|
+
const toolActorUuid = crypto.randomUUID();
|
|
10216
10879
|
const metadata = {
|
|
10217
10880
|
title: "HDF Assessment Results Export",
|
|
10218
10881
|
"last-modified": timestamp,
|
|
10219
10882
|
version: "1.0.0",
|
|
10220
|
-
"oscal-version": OSCAL_VERSION
|
|
10883
|
+
"oscal-version": OSCAL_VERSION,
|
|
10884
|
+
parties: [{
|
|
10885
|
+
uuid: toolActorUuid,
|
|
10886
|
+
type: "organization",
|
|
10887
|
+
name: toolPartyName(hdfResults)
|
|
10888
|
+
}]
|
|
10221
10889
|
};
|
|
10222
10890
|
let importAP;
|
|
10223
10891
|
if (hdfResults.planRef && hdfResults.planRef !== "") importAP = { href: hdfResults.planRef };
|
|
10224
10892
|
else importAP = { href: "#" };
|
|
10225
10893
|
const results = [];
|
|
10226
|
-
for (const baseline of hdfResults.baselines) results.push(baselineToResult(baseline, timestamp));
|
|
10894
|
+
for (const baseline of hdfResults.baselines) results.push(baselineToResult(baseline, timestamp, toolActorUuid));
|
|
10227
10895
|
return { "assessment-results": {
|
|
10228
10896
|
uuid: crypto.randomUUID(),
|
|
10229
10897
|
metadata,
|
|
@@ -10262,7 +10930,7 @@ function assessmentStart(baseline, fallback) {
|
|
|
10262
10930
|
/**
|
|
10263
10931
|
* Converts a single EvaluatedBaseline to an OSCAL Result.
|
|
10264
10932
|
*/
|
|
10265
|
-
function baselineToResult(baseline, timestamp) {
|
|
10933
|
+
function baselineToResult(baseline, timestamp, toolActorUuid) {
|
|
10266
10934
|
let title = baseline.name;
|
|
10267
10935
|
if (baseline.title && baseline.title !== "") title = baseline.title;
|
|
10268
10936
|
let description = "Converted from HDF results";
|
|
@@ -10270,17 +10938,26 @@ function baselineToResult(baseline, timestamp) {
|
|
|
10270
10938
|
const findings = [];
|
|
10271
10939
|
const observations = [];
|
|
10272
10940
|
const risks = [];
|
|
10941
|
+
const includeControls = [];
|
|
10942
|
+
const seenControl = /* @__PURE__ */ new Set();
|
|
10273
10943
|
for (const req of baseline.requirements) {
|
|
10274
|
-
const { finding, observation, risk } = requirementToFindingSet(req, timestamp);
|
|
10944
|
+
const { finding, observation, risk } = requirementToFindingSet(req, timestamp, toolActorUuid);
|
|
10275
10945
|
findings.push(finding);
|
|
10276
10946
|
if (observation) observations.push(observation);
|
|
10277
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
|
+
}
|
|
10278
10953
|
}
|
|
10954
|
+
const controlSelection = includeControls.length > 0 ? { "include-controls": includeControls } : {};
|
|
10279
10955
|
return {
|
|
10280
10956
|
uuid: crypto.randomUUID(),
|
|
10281
10957
|
title,
|
|
10282
10958
|
description,
|
|
10283
10959
|
start: assessmentStart(baseline, timestamp),
|
|
10960
|
+
"reviewed-controls": { "control-selections": [controlSelection] },
|
|
10284
10961
|
findings,
|
|
10285
10962
|
observations,
|
|
10286
10963
|
risks
|
|
@@ -10289,49 +10966,39 @@ function baselineToResult(baseline, timestamp) {
|
|
|
10289
10966
|
/**
|
|
10290
10967
|
* Converts an EvaluatedRequirement into a Finding, optional Observation, and optional Risk.
|
|
10291
10968
|
*/
|
|
10292
|
-
function requirementToFindingSet(req, timestamp) {
|
|
10969
|
+
function requirementToFindingSet(req, timestamp, toolActorUuid) {
|
|
10293
10970
|
const controlID = nistTagToControlId(req.id);
|
|
10294
|
-
const
|
|
10295
|
-
const
|
|
10971
|
+
const results = req.results ?? [];
|
|
10972
|
+
const descriptions = req.descriptions ?? [];
|
|
10973
|
+
const { state, reason } = aggregateStatus(results);
|
|
10974
|
+
const findingDesc = extractDefaultDescription(descriptions);
|
|
10296
10975
|
const props = [];
|
|
10976
|
+
const addProp = (name, value) => {
|
|
10977
|
+
if (value !== "") props.push({
|
|
10978
|
+
name,
|
|
10979
|
+
value
|
|
10980
|
+
});
|
|
10981
|
+
};
|
|
10297
10982
|
const pushTagValues = (key) => {
|
|
10298
10983
|
const raw = req.tags?.[key];
|
|
10299
10984
|
if (Array.isArray(raw)) {
|
|
10300
|
-
for (const v of raw) if (typeof v === "string")
|
|
10301
|
-
name: key,
|
|
10302
|
-
value: v
|
|
10303
|
-
});
|
|
10985
|
+
for (const v of raw) if (typeof v === "string") addProp(key, v);
|
|
10304
10986
|
}
|
|
10305
10987
|
};
|
|
10306
10988
|
pushTagValues("nist");
|
|
10307
10989
|
pushTagValues("cci");
|
|
10308
|
-
if (req.code)
|
|
10309
|
-
name: "code",
|
|
10310
|
-
value: req.code
|
|
10311
|
-
});
|
|
10990
|
+
if (req.code != null) addProp("code", req.code);
|
|
10312
10991
|
for (const label of [
|
|
10313
10992
|
"check",
|
|
10314
10993
|
"fix",
|
|
10315
10994
|
"rationale"
|
|
10316
10995
|
]) {
|
|
10317
|
-
const d =
|
|
10318
|
-
|
|
10319
|
-
name: label,
|
|
10320
|
-
value: d.data
|
|
10321
|
-
});
|
|
10996
|
+
const d = descriptions.find((x) => x.label === label);
|
|
10997
|
+
addProp(label, d ? d.data : "");
|
|
10322
10998
|
}
|
|
10323
|
-
if (req.controlType)
|
|
10324
|
-
|
|
10325
|
-
|
|
10326
|
-
});
|
|
10327
|
-
if (req.verificationMethod) props.push({
|
|
10328
|
-
name: "verification-method",
|
|
10329
|
-
value: req.verificationMethod
|
|
10330
|
-
});
|
|
10331
|
-
if (req.applicability) props.push({
|
|
10332
|
-
name: "applicability",
|
|
10333
|
-
value: req.applicability
|
|
10334
|
-
});
|
|
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);
|
|
10335
11002
|
const links = [];
|
|
10336
11003
|
if (Array.isArray(req.refs)) for (const r of req.refs) {
|
|
10337
11004
|
const o = r;
|
|
@@ -10360,19 +11027,19 @@ function requirementToFindingSet(req, timestamp) {
|
|
|
10360
11027
|
const finding = {
|
|
10361
11028
|
uuid: crypto.randomUUID(),
|
|
10362
11029
|
title,
|
|
10363
|
-
description: findingDesc ||
|
|
11030
|
+
description: findingDesc || title,
|
|
10364
11031
|
props: props.length > 0 ? props : void 0,
|
|
10365
11032
|
links: links.length > 0 ? links : void 0,
|
|
10366
11033
|
target
|
|
10367
11034
|
};
|
|
10368
11035
|
let observation;
|
|
10369
|
-
if (
|
|
11036
|
+
if (results.length > 0) {
|
|
10370
11037
|
const obsUUID = crypto.randomUUID();
|
|
10371
11038
|
observation = {
|
|
10372
11039
|
uuid: obsUUID,
|
|
10373
|
-
description: buildObservationDescription(
|
|
11040
|
+
description: buildObservationDescription(results),
|
|
10374
11041
|
methods: ["TEST"],
|
|
10375
|
-
collected: formatAssessmentTime(earliestResultTime(
|
|
11042
|
+
collected: formatAssessmentTime(earliestResultTime(results), timestamp)
|
|
10376
11043
|
};
|
|
10377
11044
|
finding["related-observations"] = [{ "observation-uuid": obsUUID }];
|
|
10378
11045
|
}
|
|
@@ -10386,11 +11053,17 @@ function requirementToFindingSet(req, timestamp) {
|
|
|
10386
11053
|
description: `Impact: ${req.impact.toFixed(1)} (${severity})`,
|
|
10387
11054
|
statement: `Impact: ${req.impact.toFixed(1)} (${severity})`,
|
|
10388
11055
|
status: riskStatusFromState(state),
|
|
10389
|
-
characterizations: [{
|
|
10390
|
-
|
|
10391
|
-
|
|
10392
|
-
|
|
10393
|
-
|
|
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
|
+
}]
|
|
10394
11067
|
};
|
|
10395
11068
|
finding["related-risks"] = [{ "risk-uuid": riskUUID }];
|
|
10396
11069
|
}
|
|
@@ -10690,15 +11363,56 @@ function buildTags(dep) {
|
|
|
10690
11363
|
if (dep.parentDependencies.length > 0) extras.parentDependencies = dep.parentDependencies;
|
|
10691
11364
|
return buildNistCciTags(nist, cciTags, extras);
|
|
10692
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
|
+
}
|
|
10693
11401
|
async function convertIonchannelToHdf(input, converterVersion = "1.0.0") {
|
|
10694
11402
|
if (!input?.trim()) throw new Error("Empty input");
|
|
10695
11403
|
validateInputSize(input, "ionchannel");
|
|
10696
11404
|
const analysis = parseJSON(input);
|
|
10697
11405
|
if (!Array.isArray(analysis.scan_summaries)) throw new Error(`Ion Channel scan_summaries invalid summary data (expecting array, got ${typeof analysis.scan_summaries})`);
|
|
10698
11406
|
let allDeps = [];
|
|
10699
|
-
|
|
10700
|
-
|
|
10701
|
-
|
|
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);
|
|
10702
11416
|
}
|
|
10703
11417
|
const requirements = buildDependencyGraph(allDeps).map((dep) => {
|
|
10704
11418
|
const depId = `dependency-${dep.org}/${dep.name}`;
|
|
@@ -10739,12 +11453,27 @@ async function convertIonchannelToHdf(input, converterVersion = "1.0.0") {
|
|
|
10739
11453
|
status: "loaded"
|
|
10740
11454
|
});
|
|
10741
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
|
+
}
|
|
10742
11472
|
return buildHdfResults({
|
|
10743
11473
|
generatorName: "ionchannel-to-hdf",
|
|
10744
11474
|
converterVersion,
|
|
10745
11475
|
toolName: "Ion Channel",
|
|
10746
|
-
|
|
10747
|
-
baselines: [baseline],
|
|
11476
|
+
baselines,
|
|
10748
11477
|
timestamp: /* @__PURE__ */ new Date()
|
|
10749
11478
|
});
|
|
10750
11479
|
}
|
|
@@ -11717,7 +12446,8 @@ function buildDocument(amendments, converterVersion) {
|
|
|
11717
12446
|
...notes.length > 0 && { notes },
|
|
11718
12447
|
publisher: {
|
|
11719
12448
|
category: "other",
|
|
11720
|
-
name: publisherName
|
|
12449
|
+
name: publisherName,
|
|
12450
|
+
namespace: "https://saf.mitre.org"
|
|
11721
12451
|
},
|
|
11722
12452
|
tracking: {
|
|
11723
12453
|
id: trackingId,
|
|
@@ -12027,7 +12757,11 @@ function buildMetadata(a, docTime, converterVersion) {
|
|
|
12027
12757
|
}
|
|
12028
12758
|
async function buildSerialNumber(input, a) {
|
|
12029
12759
|
if (a.amendmentId) return `urn:uuid:${a.amendmentId}`;
|
|
12030
|
-
|
|
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)}`;
|
|
12031
12765
|
}
|
|
12032
12766
|
//#endregion
|
|
12033
12767
|
//#region converters/oscal-to-hdf/typescript/detect.ts
|
|
@@ -13050,6 +13784,225 @@ function substituteParams(text, overrides) {
|
|
|
13050
13784
|
return result;
|
|
13051
13785
|
}
|
|
13052
13786
|
//#endregion
|
|
13053
|
-
|
|
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 };
|
|
13054
14007
|
|
|
13055
14008
|
//# sourceMappingURL=index.js.map
|