@mitre/hdf-generators 3.1.0 → 3.2.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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { BaselineRequirement, HdfBaseline } from "@mitre/hdf-schema";
1
+ import { BaselineRequirement, Description, HdfBaseline, Reference } from "@mitre/hdf-schema";
2
2
 
3
3
  //#region src/types.d.ts
4
4
  /**
@@ -31,6 +31,124 @@ interface InSpecProfile {
31
31
  controls: Map<string, string>;
32
32
  }
33
33
  //#endregion
34
+ //#region src/merge.d.ts
35
+ type PreferSide = 'current' | 'upstream' | undefined;
36
+ /**
37
+ * Smart-merge a current and upstream requirement.
38
+ *
39
+ * Default (no prefer):
40
+ * - ID: always upstream
41
+ * - Scalars (title, impact, severity): upstream wins
42
+ * - Tags: union, upstream wins key conflicts
43
+ * - Descriptions: union by label, upstream wins on same label
44
+ * - Code: current (preserve tests)
45
+ * - Refs: union (deduplicated)
46
+ *
47
+ * prefer "current": scalars from current, current wins tag/desc conflicts
48
+ * prefer "upstream": everything from upstream (full replacement)
49
+ */
50
+ declare function mergeRequirement(current: BaselineRequirement, upstream: BaselineRequirement, prefer?: PreferSide): BaselineRequirement;
51
+ /**
52
+ * Merge two tag maps.
53
+ *
54
+ * Default: union of keys; upstream wins on key conflicts.
55
+ * prefer "current": union; current wins on key conflicts.
56
+ * prefer "upstream": upstream replaces all.
57
+ */
58
+ declare function mergeTags(current: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
59
+ upstream: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
60
+ prefer?: PreferSide): Record<string, any>;
61
+ /**
62
+ * Merge two description arrays by label.
63
+ *
64
+ * Default: union by label; upstream wins on label conflicts.
65
+ * prefer "current": union; current wins on label conflicts.
66
+ * prefer "upstream": upstream replaces all.
67
+ */
68
+ declare function mergeDescriptions(current: Description[], upstream: Description[], prefer?: PreferSide): Description[];
69
+ /**
70
+ * Merge two reference arrays.
71
+ *
72
+ * Default: union, deduplicated by JSON key.
73
+ * prefer "current": current only.
74
+ * prefer "upstream": upstream only.
75
+ */
76
+ declare function mergeRefs(current?: Reference[], upstream?: Reference[], prefer?: PreferSide): Reference[] | undefined;
77
+ //#endregion
78
+ //#region src/delta-types.d.ts
79
+ /**
80
+ * A structured record linking an old requirement to a new one.
81
+ */
82
+ interface LinkRecord {
83
+ oldId: string | null;
84
+ newId: string;
85
+ matchMethod: string;
86
+ confidence: number;
87
+ relationship: 'primary' | 'related' | 'no-match';
88
+ srg?: string | null;
89
+ potentialMismatch: boolean;
90
+ }
91
+ /**
92
+ * Statistics summarizing the delta operation.
93
+ *
94
+ * SAF CLI-compatible counter semantics:
95
+ * - match: confident primary matches (potentialMismatch=false)
96
+ * - posMisMatch: weak primary matches (potentialMismatch=true)
97
+ * - dupMatch: related matches (shares body with a primary)
98
+ * - noMatch: no match found
99
+ *
100
+ * Invariants:
101
+ * - match + posMisMatch + dupMatch = totalMappedControls
102
+ * - totalMappedControls + noMatch = newControlsLength
103
+ */
104
+ interface DeltaStatistics {
105
+ oldControlsLength: number;
106
+ newControlsLength: number;
107
+ totalMappedControls: number;
108
+ match: number;
109
+ posMisMatch: number;
110
+ dupMatch: number;
111
+ noMatch: number;
112
+ }
113
+ /**
114
+ * Complete result of an upgrade/delta operation.
115
+ */
116
+ interface UpgradeResult {
117
+ baseline: HdfBaseline;
118
+ profile?: InSpecProfile;
119
+ linkRecords: LinkRecord[];
120
+ statistics: DeltaStatistics;
121
+ }
122
+ /** Backward-compatible alias. */
123
+ type DeltaResult = UpgradeResult;
124
+ /**
125
+ * Options for controlling upgrade/delta generation.
126
+ */
127
+ interface UpgradeOptions {
128
+ /** Conflict resolution: "current", "upstream", or undefined (smart merge). */
129
+ prefer?: PreferSide;
130
+ /** Don't preserve old test code (generate stubs for everything). */
131
+ noCode?: boolean;
132
+ /** Output format: "baseline", "inspec", or "both". Default: produce both. */
133
+ outputFormat?: string;
134
+ /** Put all controls in a single controls/controls.rb file. */
135
+ singleFile?: boolean;
136
+ /** Override profile metadata in the generated inspec.yml. */
137
+ metadata?: ProfileMetadata;
138
+ /** InSpec version constraint for inspec.yml. */
139
+ inspecVersion?: string;
140
+ /**
141
+ * Preserve current requirements that have no upstream match. Default
142
+ * (false) drops them — matching SAF CLI delta: a control DISA removed
143
+ * in the new XCCDF should be removed from the upgraded profile too.
144
+ * Set true when carrying custom controls outside the DISA STIG, or
145
+ * to inspect what got dropped before committing to the upgrade.
146
+ */
147
+ keepUnmatched?: boolean;
148
+ }
149
+ /** Backward-compatible alias. */
150
+ type DeltaOptions = UpgradeOptions;
151
+ //#endregion
34
152
  //#region src/ruby-escape.d.ts
35
153
  /**
36
154
  * Escape a string for use as a Ruby string literal.
@@ -80,5 +198,45 @@ declare function generateInSpecYml(baseline: HdfBaseline, options?: GeneratorOpt
80
198
  */
81
199
  declare function generateInSpecProfile(baseline: HdfBaseline, options?: GeneratorOptions): InSpecProfile;
82
200
  //#endregion
83
- export { type GeneratorOptions, type InSpecProfile, type ProfileMetadata, escapeQuotes, generateControlStub, generateInSpecProfile, generateInSpecYml };
201
+ //#region src/delta.d.ts
202
+ /**
203
+ * Generate an upgraded HDF Baseline by smart-merging current and upstream requirements.
204
+ *
205
+ * For each upstream requirement:
206
+ * - If matched: smart-merge current + upstream fields per mergeRequirement semantics
207
+ * - If unmatched: include upstream requirement as-is (new control)
208
+ *
209
+ * Current requirements with no upstream match are dropped by default
210
+ * (a control removed from upstream should not survive the upgrade).
211
+ * Set opts.keepUnmatched to retain them instead.
212
+ */
213
+ declare function generateUpgrade(currentBaseline: HdfBaseline, upstreamBaseline: HdfBaseline, linkRecords: LinkRecord[], opts?: UpgradeOptions): UpgradeResult;
214
+ /**
215
+ * Legacy entry point. Wraps generateUpgrade for backward compatibility.
216
+ *
217
+ * @deprecated Use generateUpgrade instead.
218
+ */
219
+ declare function generateDelta(newBaseline: HdfBaseline, linkRecords: LinkRecord[], oldCodeMap: Map<string, string>, opts?: DeltaOptions, oldControlCount?: number): DeltaResult;
220
+ //#endregion
221
+ //#region src/delta-report.d.ts
222
+ /**
223
+ * JSON report payload for delta operations.
224
+ * SAF CLI-compatible: { links: LinkRecord[] }
225
+ */
226
+ interface DeltaJsonReport {
227
+ links: LinkRecord[];
228
+ }
229
+ /**
230
+ * Generate a structured JSON report from a delta result.
231
+ * Matches SAF CLI's delta.json format: { ...diff, links }.
232
+ */
233
+ declare function generateDeltaJson(result: DeltaResult): DeltaJsonReport;
234
+ /**
235
+ * Generate a Markdown report from a delta result.
236
+ * Matches SAF CLI's delta.md format: mapping table, control counts,
237
+ * match statistics, and statistics validation.
238
+ */
239
+ declare function generateDeltaMarkdown(result: DeltaResult): string;
240
+ //#endregion
241
+ export { type DeltaJsonReport, type DeltaOptions, type DeltaResult, type DeltaStatistics, type GeneratorOptions, type InSpecProfile, type LinkRecord, type PreferSide, type ProfileMetadata, type UpgradeOptions, type UpgradeResult, escapeQuotes, generateControlStub, generateDelta, generateDeltaJson, generateDeltaMarkdown, generateInSpecProfile, generateInSpecYml, generateUpgrade, mergeDescriptions, mergeRefs, mergeRequirement, mergeTags };
84
242
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/ruby-escape.ts","../src/control-stub.ts","../src/inspec-yml.ts","../src/profile-generator.ts"],"mappings":";;;;;;UAGiB,gBAAA;EAAgB;EAE/B,UAAA;EAE0B;EAA1B,QAAA,GAAW,eAAA;EAAX;EAEA,aAAA;AAAA;;;AAMF;UAAiB,eAAA;EACf,UAAA;EACA,SAAA;EACA,OAAA;EACA,OAAA;AAAA;;;;UAMe,aAAA;EAAa;EAE5B,SAAA;EAEa;EAAb,QAAA,EAAU,GAAA;AAAA;;;;;;AA1BZ;;;;;;;;iBCQgB,YAAA,CAAa,CAAA;;;;;ADR7B;;;;;;;;;;AAYA;iBECgB,mBAAA,CAAoB,GAAA,EAAK,mBAAA;;;;AFbzC;;;;;iBGMgB,iBAAA,CACd,QAAA,EAAU,WAAA,EACV,OAAA,GAAU,gBAAA;;;;AHRZ;;;;;;iBISgB,qBAAA,CACd,QAAA,EAAU,WAAA,EACV,OAAA,GAAU,gBAAA,GACT,aAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/merge.ts","../src/delta-types.ts","../src/ruby-escape.ts","../src/control-stub.ts","../src/inspec-yml.ts","../src/profile-generator.ts","../src/delta.ts","../src/delta-report.ts"],"mappings":";;;;;;UAGiB,gBAAA;EAAgB;EAE/B,UAAA;EAE0B;EAA1B,QAAA,GAAW,eAAe;EAA1B;EAEA,aAAA;AAAA;;AAAa;AAMf;UAAiB,eAAA;EACf,UAAA;EACA,SAAA;EACA,OAAA;EACA,OAAA;AAAA;;;AAAO;UAMQ,aAAA;EAAa;EAE5B,SAAA;EAEa;EAAb,QAAA,EAAU,GAAG;AAAA;;;KC3BH,UAAA;;ADCZ;;;;;;;;;AAMe;AAMf;;;iBCGgB,gBAAA,CACd,OAAA,EAAS,mBAAA,EACT,QAAA,EAAU,mBAAA,EACV,MAAA,GAAS,UAAA,GACR,mBAAA;;;;;;ADHM;AAMT;iBCqCgB,SAAA,CACd,OAAA,EAAS,MAAA;AACT,QAAA,EAAU,MAAA;AACV,MAAA,GAAS,UAAA,GACR,MAAA;;;;;ADrCY;;;iBCiEC,iBAAA,CACd,OAAA,EAAS,WAAA,IACT,QAAA,EAAU,WAAA,IACV,MAAA,GAAS,UAAA,GACR,WAAA;AAhGH;;;;AAAsB;AAgBtB;;AAhBA,iBAqIgB,SAAA,CACd,OAAA,GAAU,SAAA,IACV,QAAA,GAAW,SAAA,IACX,MAAA,GAAS,UAAA,GACR,SAAA;;;ADxIH;;;AAAA,UEIiB,UAAA;EACf,KAAA;EACA,KAAA;EACA,WAAA;EACA,UAAA;EACA,YAAA;EACA,GAAA;EACA,iBAAA;AAAA;;;;;;;;AFKO;AAMT;;;;;UEKiB,eAAA;EACf,iBAAA;EACA,iBAAA;EACA,mBAAA;EACA,KAAA;EACA,WAAA;EACA,QAAA;EACA,OAAA;AAAA;;;ADnCoB;UCyCL,aAAA;EACf,QAAA,EAAU,WAAA;EACV,OAAA,GAAU,aAAA;EACV,WAAA,EAAa,UAAA;EACb,UAAA,EAAY,eAAA;AAAA;;KAIF,WAAA,GAAc,aAAa;;;;UAKtB,cAAA;EDpCf;ECsCA,MAAA,GAAS,UAAA;EDrCT;ECuCA,MAAA;EDtCoB;ECwCpB,YAAA;EDAc;ECEd,UAAA;;EAEA,QAAA,GAAW,eAAe;EDFhB;ECIV,aAAA;EDFC;;;;;;;ECUD,aAAA;AAAA;;KAIU,YAAA,GAAe,cAAc;;;;;;AF7EzC;;;;;;;;iBGQgB,YAAA,CAAa,CAAS;;;;;AHRtC;;;;;;;;;AAMe;AAMf;iBIOgB,mBAAA,CAAoB,GAAwB,EAAnB,mBAAmB;;;;AJnB5D;;;;;iBKMgB,iBAAA,CACd,QAAA,EAAU,WAAA,EACV,OAAA,GAAU,gBAAgB;;;;ALR5B;;;;;;iBMSgB,qBAAA,CACd,QAAA,EAAU,WAAA,EACV,OAAA,GAAU,gBAAA,GACT,aAAA;;;;ANZH;;;;;;;;;AAMe;iBOSC,eAAA,CACd,eAAA,EAAiB,WAAA,EACjB,gBAAA,EAAkB,WAAA,EAClB,WAAA,EAAa,UAAA,IACb,IAAA,GAAO,cAAA,GACN,aAAA;;;;;;iBAmFa,aAAA,CACd,WAAA,EAAa,WAAA,EACb,WAAA,EAAa,UAAA,IACb,UAAA,EAAY,GAAA,kBACZ,IAAA,GAAO,YAAA,EACP,eAAA,YACC,WAAA;;;;;AP7GH;;UQGiB,eAAA;EACf,KAAA,EAAO,UAAU;AAAA;;;;;iBAOH,iBAAA,CAAkB,MAAA,EAAQ,WAAA,GAAc,eAAe;ARCvE;;;;;AAAA,iBQmCgB,qBAAA,CAAsB,MAAmB,EAAX,WAAW"}
package/dist/index.js CHANGED
@@ -20,6 +20,11 @@ function escapeQuotes(s) {
20
20
  //#endregion
21
21
  //#region src/control-stub.ts
22
22
  /**
23
+ * Detects when a code string already starts with `control 'ID' do`,
24
+ * meaning it's a complete InSpec control file — not a body fragment.
25
+ */
26
+ const FULL_CONTROL_BLOCK = /^\s*control\s+['"]([^'"]+)['"]\s+do\b/;
27
+ /**
23
28
  * Generate a Ruby InSpec control stub from an HDF BaselineRequirement.
24
29
  *
25
30
  * Output follows the InSpec DSL ordering convention:
@@ -33,6 +38,15 @@ function escapeQuotes(s) {
33
38
  * end
34
39
  */
35
40
  function generateControlStub(req) {
41
+ if (req.code) {
42
+ const m = FULL_CONTROL_BLOCK.exec(req.code);
43
+ if (m && m[1] !== void 0) {
44
+ const innerID = m[1];
45
+ if (innerID === req.id) return req.code;
46
+ const start = m.index + m[0].indexOf(innerID);
47
+ return req.code.slice(0, start) + req.id + req.code.slice(start + innerID.length);
48
+ }
49
+ }
36
50
  const lines = [];
37
51
  lines.push(`control '${req.id}' do`);
38
52
  if (req.title) lines.push(` title ${escapeQuotes(req.title)}`);
@@ -169,6 +183,299 @@ function generateInSpecProfile(baseline, options) {
169
183
  };
170
184
  }
171
185
  //#endregion
172
- export { escapeQuotes, generateControlStub, generateInSpecProfile, generateInSpecYml };
186
+ //#region src/merge.ts
187
+ /**
188
+ * Smart-merge a current and upstream requirement.
189
+ *
190
+ * Default (no prefer):
191
+ * - ID: always upstream
192
+ * - Scalars (title, impact, severity): upstream wins
193
+ * - Tags: union, upstream wins key conflicts
194
+ * - Descriptions: union by label, upstream wins on same label
195
+ * - Code: current (preserve tests)
196
+ * - Refs: union (deduplicated)
197
+ *
198
+ * prefer "current": scalars from current, current wins tag/desc conflicts
199
+ * prefer "upstream": everything from upstream (full replacement)
200
+ */
201
+ function mergeRequirement(current, upstream, prefer) {
202
+ const merged = {
203
+ id: upstream.id,
204
+ title: prefer === "current" ? current.title : upstream.title,
205
+ impact: prefer === "current" ? current.impact : upstream.impact,
206
+ severity: prefer === "current" ? current.severity : upstream.severity,
207
+ tags: mergeTags(current.tags, upstream.tags, prefer),
208
+ descriptions: mergeDescriptions(current.descriptions, upstream.descriptions, prefer),
209
+ refs: mergeRefs(current.refs, upstream.refs, prefer),
210
+ code: prefer === "upstream" ? upstream.code : current.code ?? upstream.code,
211
+ sourceLocation: prefer === "current" ? current.sourceLocation : upstream.sourceLocation
212
+ };
213
+ if (merged.code === void 0) delete merged.code;
214
+ if (merged.severity === void 0) delete merged.severity;
215
+ if (merged.sourceLocation === void 0) delete merged.sourceLocation;
216
+ if (merged.refs === void 0 || merged.refs.length === 0) delete merged.refs;
217
+ return merged;
218
+ }
219
+ /**
220
+ * Merge two tag maps.
221
+ *
222
+ * Default: union of keys; upstream wins on key conflicts.
223
+ * prefer "current": union; current wins on key conflicts.
224
+ * prefer "upstream": upstream replaces all.
225
+ */
226
+ function mergeTags(current, upstream, prefer) {
227
+ if (prefer === "upstream") return { ...upstream };
228
+ const merged = { ...current };
229
+ for (const [key, value] of Object.entries(upstream)) if (prefer === "current") {
230
+ if (!(key in merged)) merged[key] = value;
231
+ } else merged[key] = value;
232
+ return merged;
233
+ }
234
+ /**
235
+ * Merge two description arrays by label.
236
+ *
237
+ * Default: union by label; upstream wins on label conflicts.
238
+ * prefer "current": union; current wins on label conflicts.
239
+ * prefer "upstream": upstream replaces all.
240
+ */
241
+ function mergeDescriptions(current, upstream, prefer) {
242
+ if (prefer === "upstream") return [...upstream];
243
+ const byLabel = /* @__PURE__ */ new Map();
244
+ const order = [];
245
+ for (const d of current) {
246
+ byLabel.set(d.label, d);
247
+ order.push(d.label);
248
+ }
249
+ for (const d of upstream) if (byLabel.has(d.label)) {
250
+ if (prefer !== "current") byLabel.set(d.label, d);
251
+ } else {
252
+ byLabel.set(d.label, d);
253
+ order.push(d.label);
254
+ }
255
+ return order.map((label) => byLabel.get(label));
256
+ }
257
+ /**
258
+ * Merge two reference arrays.
259
+ *
260
+ * Default: union, deduplicated by JSON key.
261
+ * prefer "current": current only.
262
+ * prefer "upstream": upstream only.
263
+ */
264
+ function mergeRefs(current, upstream, prefer) {
265
+ if (prefer === "current") return current ? [...current] : void 0;
266
+ if (prefer === "upstream") return upstream ? [...upstream] : void 0;
267
+ if (!current && !upstream) return void 0;
268
+ const seen = /* @__PURE__ */ new Set();
269
+ const result = [];
270
+ const addRef = (r) => {
271
+ const key = JSON.stringify(r);
272
+ if (!seen.has(key)) {
273
+ seen.add(key);
274
+ result.push(r);
275
+ }
276
+ };
277
+ if (current) current.forEach(addRef);
278
+ if (upstream) upstream.forEach(addRef);
279
+ return result.length > 0 ? result : void 0;
280
+ }
281
+ //#endregion
282
+ //#region src/delta.ts
283
+ /**
284
+ * Generate an upgraded HDF Baseline by smart-merging current and upstream requirements.
285
+ *
286
+ * For each upstream requirement:
287
+ * - If matched: smart-merge current + upstream fields per mergeRequirement semantics
288
+ * - If unmatched: include upstream requirement as-is (new control)
289
+ *
290
+ * Current requirements with no upstream match are dropped by default
291
+ * (a control removed from upstream should not survive the upgrade).
292
+ * Set opts.keepUnmatched to retain them instead.
293
+ */
294
+ function generateUpgrade(currentBaseline, upstreamBaseline, linkRecords, opts) {
295
+ const prefer = opts?.prefer;
296
+ const linkByNewId = /* @__PURE__ */ new Map();
297
+ for (const lr of linkRecords) if (!linkByNewId.get(lr.newId) || lr.relationship === "primary") linkByNewId.set(lr.newId, lr);
298
+ const currentById = /* @__PURE__ */ new Map();
299
+ for (const req of currentBaseline.requirements) currentById.set(req.id, req);
300
+ const matchedCurrentIds = /* @__PURE__ */ new Set();
301
+ const mergedReqs = [];
302
+ for (const upReq of upstreamBaseline.requirements) {
303
+ const link = linkByNewId.get(upReq.id);
304
+ if (link && link.oldId) {
305
+ const curReq = currentById.get(link.oldId);
306
+ if (curReq) {
307
+ matchedCurrentIds.add(link.oldId);
308
+ const effectiveCurrent = opts?.noCode ? {
309
+ ...curReq,
310
+ code: void 0
311
+ } : curReq;
312
+ mergedReqs.push(mergeRequirement(effectiveCurrent, upReq, prefer));
313
+ continue;
314
+ }
315
+ }
316
+ mergedReqs.push(upReq);
317
+ }
318
+ if (opts?.keepUnmatched) {
319
+ for (const curReq of currentBaseline.requirements) if (!matchedCurrentIds.has(curReq.id)) mergedReqs.push(curReq);
320
+ }
321
+ const upgradedBaseline = {
322
+ ...upstreamBaseline,
323
+ requirements: mergedReqs
324
+ };
325
+ const result = {
326
+ baseline: upgradedBaseline,
327
+ linkRecords,
328
+ statistics: computeStatistics(linkRecords, currentBaseline.requirements.length, upstreamBaseline.requirements.length)
329
+ };
330
+ const outputFormat = opts?.outputFormat ?? "";
331
+ if (outputFormat === "inspec" || outputFormat === "both" || outputFormat === "") result.profile = generateProfileFromBaseline(upgradedBaseline, opts);
332
+ return result;
333
+ }
334
+ /**
335
+ * Legacy entry point. Wraps generateUpgrade for backward compatibility.
336
+ *
337
+ * @deprecated Use generateUpgrade instead.
338
+ */
339
+ function generateDelta(newBaseline, linkRecords, oldCodeMap, opts, oldControlCount) {
340
+ const result = generateUpgrade({
341
+ name: "current",
342
+ requirements: buildCurrentReqsFromCodeMap(linkRecords, oldCodeMap),
343
+ groups: [],
344
+ supports: []
345
+ }, newBaseline, linkRecords, opts);
346
+ result.statistics.oldControlsLength = oldControlCount ?? 0;
347
+ return result;
348
+ }
349
+ function buildCurrentReqsFromCodeMap(linkRecords, oldCodeMap) {
350
+ const seen = /* @__PURE__ */ new Set();
351
+ const reqs = [];
352
+ for (const lr of linkRecords) {
353
+ if (!lr.oldId || seen.has(lr.oldId)) continue;
354
+ seen.add(lr.oldId);
355
+ const req = {
356
+ id: lr.oldId,
357
+ impact: 0,
358
+ tags: {},
359
+ descriptions: [{
360
+ label: "default",
361
+ data: ""
362
+ }]
363
+ };
364
+ const code = oldCodeMap.get(lr.oldId);
365
+ if (code) req.code = code;
366
+ reqs.push(req);
367
+ }
368
+ return reqs;
369
+ }
370
+ function generateProfileFromBaseline(baseline, opts) {
371
+ const controls = /* @__PURE__ */ new Map();
372
+ const allStubs = [];
373
+ for (const req of baseline.requirements) {
374
+ const ruby = generateControlStub(req);
375
+ if (opts?.singleFile) allStubs.push(ruby);
376
+ else {
377
+ const safeId = req.id.replace(/\.\./g, "").replace(/[/\\]/g, "") || "unknown";
378
+ controls.set(`controls/${safeId}.rb`, ruby);
379
+ }
380
+ }
381
+ if (opts?.singleFile && allStubs.length > 0) controls.set("controls/controls.rb", allStubs.join("\n"));
382
+ return {
383
+ inspecYml: generateInSpecYml(baseline, {
384
+ singleFile: opts?.singleFile,
385
+ metadata: opts?.metadata,
386
+ inspecVersion: opts?.inspecVersion
387
+ }),
388
+ controls
389
+ };
390
+ }
391
+ function computeStatistics(linkRecords, oldControlCount, newControlCount) {
392
+ let match = 0;
393
+ let posMisMatch = 0;
394
+ let dupMatch = 0;
395
+ let noMatch = 0;
396
+ for (const lr of linkRecords) if (lr.relationship === "related") dupMatch++;
397
+ else if (lr.relationship === "no-match") noMatch++;
398
+ else if (lr.potentialMismatch) posMisMatch++;
399
+ else match++;
400
+ return {
401
+ oldControlsLength: oldControlCount,
402
+ newControlsLength: newControlCount,
403
+ totalMappedControls: match + posMisMatch + dupMatch,
404
+ match,
405
+ posMisMatch,
406
+ dupMatch,
407
+ noMatch
408
+ };
409
+ }
410
+ //#endregion
411
+ //#region src/delta-report.ts
412
+ /**
413
+ * Generate a structured JSON report from a delta result.
414
+ * Matches SAF CLI's delta.json format: { ...diff, links }.
415
+ */
416
+ function generateDeltaJson(result) {
417
+ return { links: result.linkRecords };
418
+ }
419
+ /**
420
+ * Format a per-link match method description matching SAF CLI's logMatchMethod.
421
+ */
422
+ function formatMatchMethod(lr) {
423
+ const confidencePct = (lr.confidence * 100).toFixed(0) + "%";
424
+ switch (lr.matchMethod) {
425
+ case "srgDeterministic": return `SRG deterministic (${lr.srg ?? "?"}) [${lr.relationship}]`;
426
+ case "srgCciTiebreak": return `SRG block + CCI tiebreak (Jaccard=${confidencePct}) [${lr.relationship}]`;
427
+ case "vendorFuzzyTitle": return `Vendor fuzzy title (confidence=${confidencePct}) [${lr.relationship}]`;
428
+ case "exactId": return `Exact ID [${lr.relationship}]`;
429
+ case "cciMatch": return `CCI match [${lr.relationship}]`;
430
+ case "fuzzyTitle": return `Fuzzy title (confidence=${confidencePct}) [${lr.relationship}]`;
431
+ case "none": return "No match";
432
+ default: return `${lr.matchMethod} [${lr.relationship}]`;
433
+ }
434
+ }
435
+ /**
436
+ * Generate a Markdown report from a delta result.
437
+ * Matches SAF CLI's delta.md format: mapping table, control counts,
438
+ * match statistics, and statistics validation.
439
+ */
440
+ function generateDeltaMarkdown(result) {
441
+ const lines = [];
442
+ const stats = result.statistics;
443
+ if (result.linkRecords.length > 0) {
444
+ lines.push("Mapping Results ===========================================================================");
445
+ lines.push(" Old Control -> New Control");
446
+ for (const lr of result.linkRecords) if (lr.relationship !== "no-match" && lr.oldId) lines.push(`\t ${lr.oldId} -> ${lr.newId}`);
447
+ const totalMapped = stats.totalMappedControls;
448
+ lines.push(`Total Mapped Controls: ${totalMapped}`);
449
+ lines.push("");
450
+ }
451
+ lines.push("Control Counts ===========================");
452
+ lines.push(`Total Controls Available for Delta: ${stats.oldControlsLength}`);
453
+ lines.push(` Total Controls Found on XCCDF: ${stats.newControlsLength}`);
454
+ lines.push("");
455
+ lines.push("Match Statistics =========================");
456
+ lines.push(` Match Controls: ${stats.match}`);
457
+ lines.push(` Possible Mismatch Controls: ${stats.posMisMatch}`);
458
+ lines.push(` Related Match Controls: ${stats.dupMatch}`);
459
+ lines.push(` No Match Controls: ${stats.noMatch}`);
460
+ lines.push("");
461
+ lines.push("Statistics Validation =============================================");
462
+ const totalMapped = stats.totalMappedControls;
463
+ const matchMappedValid = stats.match + stats.posMisMatch + stats.dupMatch === totalMapped;
464
+ const processedValid = totalMapped + stats.noMatch === stats.newControlsLength;
465
+ lines.push(`Match + Mismatch + Related = Total Mapped Controls: (${stats.match}+${stats.posMisMatch}+${stats.dupMatch}=${totalMapped}) ${matchMappedValid}`);
466
+ lines.push(` Total Processed = Total XCCDF Controls: (${totalMapped}+${stats.noMatch}=${totalMapped + stats.noMatch}) ${processedValid}`);
467
+ lines.push("");
468
+ if (result.linkRecords.length > 0) {
469
+ lines.push("Match Details =============================================================");
470
+ for (const lr of result.linkRecords) if (lr.oldId) {
471
+ lines.push(` ${lr.oldId} --> ${lr.newId}`);
472
+ lines.push(` Match method: ${formatMatchMethod(lr)}`);
473
+ } else lines.push(` (none) --> ${lr.newId} [no match]`);
474
+ lines.push("");
475
+ }
476
+ return lines.join("\n");
477
+ }
478
+ //#endregion
479
+ export { escapeQuotes, generateControlStub, generateDelta, generateDeltaJson, generateDeltaMarkdown, generateInSpecProfile, generateInSpecYml, generateUpgrade, mergeDescriptions, mergeRefs, mergeRequirement, mergeTags };
173
480
 
174
481
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/ruby-escape.ts","../src/control-stub.ts","../src/inspec-yml.ts","../src/profile-generator.ts"],"sourcesContent":["/**\n * Escape a string for use as a Ruby string literal.\n *\n * Strategy:\n * - If the string contains both single and double quotes → %q() wrapper\n * - If the string contains only single quotes → double-quoted with escaping\n * - Otherwise → single-quoted with escaping\n *\n * Ported from ts-inspec-objects escapeQuotes(), cross-referenced with\n * inspec-parser's RubyRebuilder.\n */\nexport function escapeQuotes(s: string): string {\n const hasSingle = s.includes(\"'\");\n const hasDouble = s.includes('\"');\n\n if (hasSingle && hasDouble) {\n // %q() — escape backslashes before ) so Ruby doesn't treat \\) as escaped delimiter\n return `%q(${s.replace(/\\\\\\)/g, '\\\\\\\\)')})`;\n }\n\n if (hasSingle) {\n // Double-quoted: escape backslashes, then double quotes\n return `\"${s.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`;\n }\n\n // Single-quoted: escape backslashes, then single quotes\n return `'${s.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\")}'`;\n}\n","import type { BaselineRequirement } from '@mitre/hdf-schema';\nimport { escapeQuotes } from './ruby-escape.js';\n\n/**\n * Generate a Ruby InSpec control stub from an HDF BaselineRequirement.\n *\n * Output follows the InSpec DSL ordering convention:\n * control 'ID' do\n * title ...\n * desc ...\n * desc 'check', ...\n * impact ...\n * tag key: value\n * <code or stub comment>\n * end\n */\nexport function generateControlStub(req: BaselineRequirement): string {\n const lines: string[] = [];\n\n lines.push(`control '${req.id}' do`);\n\n // Title\n if (req.title) {\n lines.push(` title ${escapeQuotes(req.title)}`);\n }\n\n // Descriptions: default first (as bare `desc`), then labeled\n const defaultDesc = req.descriptions.find((d) => d.label === 'default');\n if (defaultDesc) {\n lines.push(` desc ${escapeQuotes(defaultDesc.data)}`);\n }\n\n const seenDefault = new Set<string>();\n for (const d of req.descriptions) {\n if (d.label === 'default') {\n if (seenDefault.has('default')) {\n continue; // skip duplicate default\n }\n seenDefault.add('default');\n continue; // already emitted above as bare desc\n }\n lines.push(` desc '${d.label}', ${escapeQuotes(d.data)}`);\n }\n\n // Impact — always render with at least one decimal place for 0 and whole numbers\n if (req.impact !== undefined) {\n const impactStr = Number.isInteger(req.impact)\n ? req.impact.toFixed(1)\n : String(req.impact);\n lines.push(` impact ${impactStr}`);\n }\n\n // Tags\n for (const [key, value] of Object.entries(req.tags)) {\n lines.push(` ${formatTag(key, value)}`);\n }\n\n // Code body or stub placeholder\n if (req.code) {\n lines.push('');\n lines.push(req.code);\n } else {\n lines.push('');\n lines.push(' # TODO: Add InSpec test code here');\n }\n\n lines.push('end');\n lines.push(''); // trailing newline\n\n return lines.join('\\n');\n}\n\n/** Format a single tag key-value pair as Ruby DSL. */\nfunction formatTag(key: string, value: unknown): string {\n if (value === null || value === undefined) {\n return `tag ${key}: nil`;\n }\n\n if (Array.isArray(value)) {\n // Ruby array syntax: tag key: ['val1', 'val2']\n const items = value.map((v: unknown) => `'${String(v)}'`).join(', ');\n return `tag ${key}: [${items}]`;\n }\n\n if (typeof value === 'boolean') {\n return `tag ${key}: ${value}`;\n }\n\n if (typeof value === 'string') {\n return `tag ${key}: ${escapeQuotes(value)}`;\n }\n\n // Fallback for other types (numbers, objects)\n return `tag ${key}: ${JSON.stringify(value)}`;\n}\n","import type { HdfBaseline } from '@mitre/hdf-schema';\nimport type { GeneratorOptions } from './types.js';\n\n/**\n * Generate an inspec.yml YAML string from an HDF Baseline and options.\n *\n * Uses string interpolation — no YAML library needed since the structure\n * is fixed and shallow.\n */\nexport function generateInSpecYml(\n baseline: HdfBaseline,\n options?: GeneratorOptions,\n): string {\n const lines: string[] = [];\n const meta = options?.metadata;\n\n // Required: name\n lines.push(`name: ${baseline.name}`);\n\n // Optional metadata fields\n if (baseline.title) {\n lines.push(`title: ${baseline.title}`);\n }\n\n const maintainer = meta?.maintainer ?? baseline.maintainer;\n if (maintainer) {\n lines.push(`maintainer: ${maintainer}`);\n }\n\n const copyright = meta?.copyright ?? baseline.copyright;\n if (copyright) {\n lines.push(`copyright: ${copyright}`);\n }\n\n const license = meta?.license ?? baseline.license;\n if (license) {\n lines.push(`license: ${license}`);\n }\n\n if (baseline.summary) {\n lines.push(`summary: ${baseline.summary}`);\n }\n\n // Version: metadata override takes priority\n const version = meta?.version ?? baseline.version;\n if (version) {\n lines.push(`version: '${version}'`);\n }\n\n // InSpec version constraint\n const inspecVersion = options?.inspecVersion ?? '~>6.0';\n lines.push(`inspec_version: '${inspecVersion}'`);\n\n // Supports array\n if (baseline.supports && baseline.supports.length > 0) {\n lines.push('supports:');\n for (const support of baseline.supports) {\n const entries: string[] = [];\n if (support.platformName) {\n entries.push(` platform-name: ${support.platformName}`);\n }\n if (support.platformFamily) {\n entries.push(` platform-family: ${support.platformFamily}`);\n }\n if (support.platform) {\n entries.push(` platform: ${support.platform}`);\n }\n if (support.release) {\n entries.push(` release: ${support.release}`);\n }\n if (entries.length > 0) {\n lines.push(`- ${entries[0]!.trimStart()}`);\n for (let i = 1; i < entries.length; i++) {\n lines.push(entries[i]!);\n }\n }\n }\n }\n\n // Depends array\n if (baseline.depends && baseline.depends.length > 0) {\n lines.push('depends:');\n for (const dep of baseline.depends) {\n const entries: string[] = [];\n if (dep.name) entries.push(`name: ${dep.name}`);\n if (dep.git) entries.push(`git: ${dep.git}`);\n if (dep.url) entries.push(`url: ${dep.url}`);\n if (dep.path) entries.push(`path: ${dep.path}`);\n if (dep.branch) entries.push(`branch: ${dep.branch}`);\n if (dep.compliance) entries.push(`compliance: ${dep.compliance}`);\n if (dep.supermarket) entries.push(`supermarket: ${dep.supermarket}`);\n if (entries.length > 0) {\n lines.push(`- ${entries[0]}`);\n for (let i = 1; i < entries.length; i++) {\n lines.push(` ${entries[i]}`);\n }\n }\n }\n }\n\n // Inputs array\n if (baseline.inputs && baseline.inputs.length > 0) {\n lines.push('inputs:');\n for (const input of baseline.inputs) {\n for (const [key, value] of Object.entries(input)) {\n lines.push(`- ${key}: ${formatYamlValue(value)}`);\n }\n }\n }\n\n lines.push(''); // trailing newline\n return lines.join('\\n');\n}\n\n/** Format a value for inline YAML output. */\nfunction formatYamlValue(value: unknown): string {\n if (typeof value === 'boolean') return String(value);\n if (typeof value === 'number') return String(value);\n if (typeof value === 'string') return value;\n return JSON.stringify(value);\n}\n","import type { HdfBaseline } from '@mitre/hdf-schema';\nimport type { GeneratorOptions, InSpecProfile } from './types.js';\nimport { generateControlStub } from './control-stub.js';\nimport { generateInSpecYml } from './inspec-yml.js';\n\n/**\n * Generate an in-memory InSpec profile from an HDF Baseline.\n *\n * Returns an InSpecProfile with inspec.yml content and a Map of\n * control filenames to Ruby source code. No file I/O — the CLI\n * is responsible for writing files to disk.\n */\nexport function generateInSpecProfile(\n baseline: HdfBaseline,\n options?: GeneratorOptions,\n): InSpecProfile {\n const inspecYml = generateInSpecYml(baseline, options);\n const controls = new Map<string, string>();\n\n if (baseline.requirements.length === 0) {\n return { inspecYml, controls };\n }\n\n if (options?.singleFile) {\n // All controls in a single file\n const stubs = baseline.requirements.map((req) => generateControlStub(req));\n controls.set('controls/controls.rb', stubs.join('\\n'));\n } else {\n // One file per control — sanitize ID for safe filenames\n for (const req of baseline.requirements) {\n const safeId = req.id.replace(/\\.\\./g, '').replace(/[/\\\\]/g, '') || 'unknown';\n const filename = `controls/${safeId}.rb`;\n controls.set(filename, generateControlStub(req));\n }\n }\n\n return { inspecYml, controls };\n}\n"],"mappings":";;;;;;;;;;;;AAWA,SAAgB,aAAa,GAAmB;CAC9C,MAAM,YAAY,EAAE,SAAS,IAAI;CACjC,MAAM,YAAY,EAAE,SAAS,KAAI;AAEjC,KAAI,aAAa,UAEf,QAAO,MAAM,EAAE,QAAQ,SAAS,QAAQ,CAAC;AAG3C,KAAI,UAEF,QAAO,IAAI,EAAE,QAAQ,OAAO,OAAO,CAAC,QAAQ,MAAM,OAAM,CAAC;AAI3D,QAAO,IAAI,EAAE,QAAQ,OAAO,OAAO,CAAC,QAAQ,MAAM,MAAM,CAAC;;;;;;;;;;;;;;;;;ACV3D,SAAgB,oBAAoB,KAAkC;CACpE,MAAM,QAAkB,EAAE;AAE1B,OAAM,KAAK,YAAY,IAAI,GAAG,MAAM;AAGpC,KAAI,IAAI,MACN,OAAM,KAAK,WAAW,aAAa,IAAI,MAAM,GAAG;CAIlD,MAAM,cAAc,IAAI,aAAa,MAAM,MAAM,EAAE,UAAU,UAAU;AACvE,KAAI,YACF,OAAM,KAAK,UAAU,aAAa,YAAY,KAAK,GAAG;CAGxD,MAAM,8BAAc,IAAI,KAAa;AACrC,MAAK,MAAM,KAAK,IAAI,cAAc;AAChC,MAAI,EAAE,UAAU,WAAW;AACzB,OAAI,YAAY,IAAI,UAAU,CAC5B;AAEF,eAAY,IAAI,UAAU;AAC1B;;AAEF,QAAM,KAAK,WAAW,EAAE,MAAM,KAAK,aAAa,EAAE,KAAK,GAAG;;AAI5D,KAAI,IAAI,WAAW,KAAA,GAAW;EAC5B,MAAM,YAAY,OAAO,UAAU,IAAI,OAAO,GAC1C,IAAI,OAAO,QAAQ,EAAE,GACrB,OAAO,IAAI,OAAO;AACtB,QAAM,KAAK,YAAY,YAAY;;AAIrC,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,IAAI,KAAK,CACjD,OAAM,KAAK,KAAK,UAAU,KAAK,MAAM,GAAG;AAI1C,KAAI,IAAI,MAAM;AACZ,QAAM,KAAK,GAAG;AACd,QAAM,KAAK,IAAI,KAAK;QACf;AACL,QAAM,KAAK,GAAG;AACd,QAAM,KAAK,sCAAsC;;AAGnD,OAAM,KAAK,MAAM;AACjB,OAAM,KAAK,GAAG;AAEd,QAAO,MAAM,KAAK,KAAK;;;AAIzB,SAAS,UAAU,KAAa,OAAwB;AACtD,KAAI,UAAU,QAAQ,UAAU,KAAA,EAC9B,QAAO,OAAO,IAAI;AAGpB,KAAI,MAAM,QAAQ,MAAM,CAGtB,QAAO,OAAO,IAAI,KADJ,MAAM,KAAK,MAAe,IAAI,OAAO,EAAE,CAAC,GAAG,CAAC,KAAK,KAAK,CACvC;AAG/B,KAAI,OAAO,UAAU,UACnB,QAAO,OAAO,IAAI,IAAI;AAGxB,KAAI,OAAO,UAAU,SACnB,QAAO,OAAO,IAAI,IAAI,aAAa,MAAM;AAI3C,QAAO,OAAO,IAAI,IAAI,KAAK,UAAU,MAAM;;;;;;;;;;ACpF7C,SAAgB,kBACd,UACA,SACQ;CACR,MAAM,QAAkB,EAAE;CAC1B,MAAM,OAAO,SAAS;AAGtB,OAAM,KAAK,SAAS,SAAS,OAAO;AAGpC,KAAI,SAAS,MACX,OAAM,KAAK,UAAU,SAAS,QAAQ;CAGxC,MAAM,aAAa,MAAM,cAAc,SAAS;AAChD,KAAI,WACF,OAAM,KAAK,eAAe,aAAa;CAGzC,MAAM,YAAY,MAAM,aAAa,SAAS;AAC9C,KAAI,UACF,OAAM,KAAK,cAAc,YAAY;CAGvC,MAAM,UAAU,MAAM,WAAW,SAAS;AAC1C,KAAI,QACF,OAAM,KAAK,YAAY,UAAU;AAGnC,KAAI,SAAS,QACX,OAAM,KAAK,YAAY,SAAS,UAAU;CAI5C,MAAM,UAAU,MAAM,WAAW,SAAS;AAC1C,KAAI,QACF,OAAM,KAAK,aAAa,QAAQ,GAAG;CAIrC,MAAM,gBAAgB,SAAS,iBAAiB;AAChD,OAAM,KAAK,oBAAoB,cAAc,GAAG;AAGhD,KAAI,SAAS,YAAY,SAAS,SAAS,SAAS,GAAG;AACrD,QAAM,KAAK,YAAY;AACvB,OAAK,MAAM,WAAW,SAAS,UAAU;GACvC,MAAM,UAAoB,EAAE;AAC5B,OAAI,QAAQ,aACV,SAAQ,KAAK,oBAAoB,QAAQ,eAAe;AAE1D,OAAI,QAAQ,eACV,SAAQ,KAAK,sBAAsB,QAAQ,iBAAiB;AAE9D,OAAI,QAAQ,SACV,SAAQ,KAAK,eAAe,QAAQ,WAAW;AAEjD,OAAI,QAAQ,QACV,SAAQ,KAAK,cAAc,QAAQ,UAAU;AAE/C,OAAI,QAAQ,SAAS,GAAG;AACtB,UAAM,KAAK,KAAK,QAAQ,GAAI,WAAW,GAAG;AAC1C,SAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,IAClC,OAAM,KAAK,QAAQ,GAAI;;;;AAO/B,KAAI,SAAS,WAAW,SAAS,QAAQ,SAAS,GAAG;AACnD,QAAM,KAAK,WAAW;AACtB,OAAK,MAAM,OAAO,SAAS,SAAS;GAClC,MAAM,UAAoB,EAAE;AAC5B,OAAI,IAAI,KAAM,SAAQ,KAAK,SAAS,IAAI,OAAO;AAC/C,OAAI,IAAI,IAAK,SAAQ,KAAK,QAAQ,IAAI,MAAM;AAC5C,OAAI,IAAI,IAAK,SAAQ,KAAK,QAAQ,IAAI,MAAM;AAC5C,OAAI,IAAI,KAAM,SAAQ,KAAK,SAAS,IAAI,OAAO;AAC/C,OAAI,IAAI,OAAQ,SAAQ,KAAK,WAAW,IAAI,SAAS;AACrD,OAAI,IAAI,WAAY,SAAQ,KAAK,eAAe,IAAI,aAAa;AACjE,OAAI,IAAI,YAAa,SAAQ,KAAK,gBAAgB,IAAI,cAAc;AACpE,OAAI,QAAQ,SAAS,GAAG;AACtB,UAAM,KAAK,KAAK,QAAQ,KAAK;AAC7B,SAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,IAClC,OAAM,KAAK,KAAK,QAAQ,KAAK;;;;AAOrC,KAAI,SAAS,UAAU,SAAS,OAAO,SAAS,GAAG;AACjD,QAAM,KAAK,UAAU;AACrB,OAAK,MAAM,SAAS,SAAS,OAC3B,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,CAC9C,OAAM,KAAK,KAAK,IAAI,IAAI,gBAAgB,MAAM,GAAG;;AAKvD,OAAM,KAAK,GAAG;AACd,QAAO,MAAM,KAAK,KAAK;;;AAIzB,SAAS,gBAAgB,OAAwB;AAC/C,KAAI,OAAO,UAAU,UAAW,QAAO,OAAO,MAAM;AACpD,KAAI,OAAO,UAAU,SAAU,QAAO,OAAO,MAAM;AACnD,KAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAO,KAAK,UAAU,MAAM;;;;;;;;;;;AC3G9B,SAAgB,sBACd,UACA,SACe;CACf,MAAM,YAAY,kBAAkB,UAAU,QAAQ;CACtD,MAAM,2BAAW,IAAI,KAAqB;AAE1C,KAAI,SAAS,aAAa,WAAW,EACnC,QAAO;EAAE;EAAW;EAAU;AAGhC,KAAI,SAAS,YAAY;EAEvB,MAAM,QAAQ,SAAS,aAAa,KAAK,QAAQ,oBAAoB,IAAI,CAAC;AAC1E,WAAS,IAAI,wBAAwB,MAAM,KAAK,KAAK,CAAC;OAGtD,MAAK,MAAM,OAAO,SAAS,cAAc;EAEvC,MAAM,WAAW,YADF,IAAI,GAAG,QAAQ,SAAS,GAAG,CAAC,QAAQ,UAAU,GAAG,IAAI,UAChC;AACpC,WAAS,IAAI,UAAU,oBAAoB,IAAI,CAAC;;AAIpD,QAAO;EAAE;EAAW;EAAU"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/ruby-escape.ts","../src/control-stub.ts","../src/inspec-yml.ts","../src/profile-generator.ts","../src/merge.ts","../src/delta.ts","../src/delta-report.ts"],"sourcesContent":["/**\n * Escape a string for use as a Ruby string literal.\n *\n * Strategy:\n * - If the string contains both single and double quotes → %q() wrapper\n * - If the string contains only single quotes → double-quoted with escaping\n * - Otherwise → single-quoted with escaping\n *\n * Ported from ts-inspec-objects escapeQuotes(), cross-referenced with\n * inspec-parser's RubyRebuilder.\n */\nexport function escapeQuotes(s: string): string {\n const hasSingle = s.includes(\"'\");\n const hasDouble = s.includes('\"');\n\n if (hasSingle && hasDouble) {\n // %q() — escape backslashes before ) so Ruby doesn't treat \\) as escaped delimiter\n return `%q(${s.replace(/\\\\\\)/g, '\\\\\\\\)')})`;\n }\n\n if (hasSingle) {\n // Double-quoted: escape backslashes, then double quotes\n return `\"${s.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`;\n }\n\n // Single-quoted: escape backslashes, then single quotes\n return `'${s.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\")}'`;\n}\n","import type { BaselineRequirement } from '@mitre/hdf-schema';\nimport { escapeQuotes } from './ruby-escape.js';\n\n/**\n * Detects when a code string already starts with `control 'ID' do`,\n * meaning it's a complete InSpec control file — not a body fragment.\n */\nconst FULL_CONTROL_BLOCK = /^\\s*control\\s+['\"]([^'\"]+)['\"]\\s+do\\b/;\n\n/**\n * Generate a Ruby InSpec control stub from an HDF BaselineRequirement.\n *\n * Output follows the InSpec DSL ordering convention:\n * control 'ID' do\n * title ...\n * desc ...\n * desc 'check', ...\n * impact ...\n * tag key: value\n * <code or stub comment>\n * end\n */\nexport function generateControlStub(req: BaselineRequirement): string {\n // If code is already a complete `control 'ID' do ... end` block (e.g.\n // from `-c controls/` reading whole .rb files), emit it as-is — wrapping\n // it again would produce nested control blocks, which InSpec rejects.\n // When the inner ID differs from req.id (e.g. an upgrade rename match\n // where current's code was carried into a renamed requirement), rewrite\n // the wrapper to match req.id so the file remains valid InSpec.\n if (req.code) {\n const m = FULL_CONTROL_BLOCK.exec(req.code);\n if (m && m[1] !== undefined) {\n const innerID = m[1];\n if (innerID === req.id) {\n return req.code;\n }\n const start = m.index + m[0].indexOf(innerID);\n return req.code.slice(0, start) + req.id + req.code.slice(start + innerID.length);\n }\n }\n\n const lines: string[] = [];\n\n lines.push(`control '${req.id}' do`);\n\n // Title\n if (req.title) {\n lines.push(` title ${escapeQuotes(req.title)}`);\n }\n\n // Descriptions: default first (as bare `desc`), then labeled\n const defaultDesc = req.descriptions.find((d) => d.label === 'default');\n if (defaultDesc) {\n lines.push(` desc ${escapeQuotes(defaultDesc.data)}`);\n }\n\n const seenDefault = new Set<string>();\n for (const d of req.descriptions) {\n if (d.label === 'default') {\n if (seenDefault.has('default')) {\n continue; // skip duplicate default\n }\n seenDefault.add('default');\n continue; // already emitted above as bare desc\n }\n lines.push(` desc '${d.label}', ${escapeQuotes(d.data)}`);\n }\n\n // Impact — always render with at least one decimal place for 0 and whole numbers\n if (req.impact !== undefined) {\n const impactStr = Number.isInteger(req.impact)\n ? req.impact.toFixed(1)\n : String(req.impact);\n lines.push(` impact ${impactStr}`);\n }\n\n // Tags\n for (const [key, value] of Object.entries(req.tags)) {\n lines.push(` ${formatTag(key, value)}`);\n }\n\n // Code body or stub placeholder\n if (req.code) {\n lines.push('');\n lines.push(req.code);\n } else {\n lines.push('');\n lines.push(' # TODO: Add InSpec test code here');\n }\n\n lines.push('end');\n lines.push(''); // trailing newline\n\n return lines.join('\\n');\n}\n\n/** Format a single tag key-value pair as Ruby DSL. */\nfunction formatTag(key: string, value: unknown): string {\n if (value === null || value === undefined) {\n return `tag ${key}: nil`;\n }\n\n if (Array.isArray(value)) {\n // Ruby array syntax: tag key: ['val1', 'val2']\n const items = value.map((v: unknown) => `'${String(v)}'`).join(', ');\n return `tag ${key}: [${items}]`;\n }\n\n if (typeof value === 'boolean') {\n return `tag ${key}: ${value}`;\n }\n\n if (typeof value === 'string') {\n return `tag ${key}: ${escapeQuotes(value)}`;\n }\n\n // Fallback for other types (numbers, objects)\n return `tag ${key}: ${JSON.stringify(value)}`;\n}\n","import type { HdfBaseline } from '@mitre/hdf-schema';\nimport type { GeneratorOptions } from './types.js';\n\n/**\n * Generate an inspec.yml YAML string from an HDF Baseline and options.\n *\n * Uses string interpolation — no YAML library needed since the structure\n * is fixed and shallow.\n */\nexport function generateInSpecYml(\n baseline: HdfBaseline,\n options?: GeneratorOptions,\n): string {\n const lines: string[] = [];\n const meta = options?.metadata;\n\n // Required: name\n lines.push(`name: ${baseline.name}`);\n\n // Optional metadata fields\n if (baseline.title) {\n lines.push(`title: ${baseline.title}`);\n }\n\n const maintainer = meta?.maintainer ?? baseline.maintainer;\n if (maintainer) {\n lines.push(`maintainer: ${maintainer}`);\n }\n\n const copyright = meta?.copyright ?? baseline.copyright;\n if (copyright) {\n lines.push(`copyright: ${copyright}`);\n }\n\n const license = meta?.license ?? baseline.license;\n if (license) {\n lines.push(`license: ${license}`);\n }\n\n if (baseline.summary) {\n lines.push(`summary: ${baseline.summary}`);\n }\n\n // Version: metadata override takes priority\n const version = meta?.version ?? baseline.version;\n if (version) {\n lines.push(`version: '${version}'`);\n }\n\n // InSpec version constraint\n const inspecVersion = options?.inspecVersion ?? '~>6.0';\n lines.push(`inspec_version: '${inspecVersion}'`);\n\n // Supports array\n if (baseline.supports && baseline.supports.length > 0) {\n lines.push('supports:');\n for (const support of baseline.supports) {\n const entries: string[] = [];\n if (support.platformName) {\n entries.push(` platform-name: ${support.platformName}`);\n }\n if (support.platformFamily) {\n entries.push(` platform-family: ${support.platformFamily}`);\n }\n if (support.platform) {\n entries.push(` platform: ${support.platform}`);\n }\n if (support.release) {\n entries.push(` release: ${support.release}`);\n }\n if (entries.length > 0) {\n lines.push(`- ${entries[0]!.trimStart()}`);\n for (let i = 1; i < entries.length; i++) {\n lines.push(entries[i]!);\n }\n }\n }\n }\n\n // Depends array\n if (baseline.depends && baseline.depends.length > 0) {\n lines.push('depends:');\n for (const dep of baseline.depends) {\n const entries: string[] = [];\n if (dep.name) entries.push(`name: ${dep.name}`);\n if (dep.git) entries.push(`git: ${dep.git}`);\n if (dep.url) entries.push(`url: ${dep.url}`);\n if (dep.path) entries.push(`path: ${dep.path}`);\n if (dep.branch) entries.push(`branch: ${dep.branch}`);\n if (dep.compliance) entries.push(`compliance: ${dep.compliance}`);\n if (dep.supermarket) entries.push(`supermarket: ${dep.supermarket}`);\n if (entries.length > 0) {\n lines.push(`- ${entries[0]}`);\n for (let i = 1; i < entries.length; i++) {\n lines.push(` ${entries[i]}`);\n }\n }\n }\n }\n\n // Inputs array\n if (baseline.inputs && baseline.inputs.length > 0) {\n lines.push('inputs:');\n for (const input of baseline.inputs) {\n for (const [key, value] of Object.entries(input)) {\n lines.push(`- ${key}: ${formatYamlValue(value)}`);\n }\n }\n }\n\n lines.push(''); // trailing newline\n return lines.join('\\n');\n}\n\n/** Format a value for inline YAML output. */\nfunction formatYamlValue(value: unknown): string {\n if (typeof value === 'boolean') return String(value);\n if (typeof value === 'number') return String(value);\n if (typeof value === 'string') return value;\n return JSON.stringify(value);\n}\n","import type { HdfBaseline } from '@mitre/hdf-schema';\nimport type { GeneratorOptions, InSpecProfile } from './types.js';\nimport { generateControlStub } from './control-stub.js';\nimport { generateInSpecYml } from './inspec-yml.js';\n\n/**\n * Generate an in-memory InSpec profile from an HDF Baseline.\n *\n * Returns an InSpecProfile with inspec.yml content and a Map of\n * control filenames to Ruby source code. No file I/O — the CLI\n * is responsible for writing files to disk.\n */\nexport function generateInSpecProfile(\n baseline: HdfBaseline,\n options?: GeneratorOptions,\n): InSpecProfile {\n const inspecYml = generateInSpecYml(baseline, options);\n const controls = new Map<string, string>();\n\n if (baseline.requirements.length === 0) {\n return { inspecYml, controls };\n }\n\n if (options?.singleFile) {\n // All controls in a single file\n const stubs = baseline.requirements.map((req) => generateControlStub(req));\n controls.set('controls/controls.rb', stubs.join('\\n'));\n } else {\n // One file per control — sanitize ID for safe filenames\n for (const req of baseline.requirements) {\n const safeId = req.id.replace(/\\.\\./g, '').replace(/[/\\\\]/g, '') || 'unknown';\n const filename = `controls/${safeId}.rb`;\n controls.set(filename, generateControlStub(req));\n }\n }\n\n return { inspecYml, controls };\n}\n","import type { BaselineRequirement, Description, Reference } from '@mitre/hdf-schema';\n\nexport type PreferSide = 'current' | 'upstream' | undefined;\n\n/**\n * Smart-merge a current and upstream requirement.\n *\n * Default (no prefer):\n * - ID: always upstream\n * - Scalars (title, impact, severity): upstream wins\n * - Tags: union, upstream wins key conflicts\n * - Descriptions: union by label, upstream wins on same label\n * - Code: current (preserve tests)\n * - Refs: union (deduplicated)\n *\n * prefer \"current\": scalars from current, current wins tag/desc conflicts\n * prefer \"upstream\": everything from upstream (full replacement)\n */\nexport function mergeRequirement(\n current: BaselineRequirement,\n upstream: BaselineRequirement,\n prefer?: PreferSide,\n): BaselineRequirement {\n const merged: BaselineRequirement = {\n // ID always comes from upstream\n id: upstream.id,\n\n // Scalars\n title: prefer === 'current' ? current.title : upstream.title,\n impact: prefer === 'current' ? current.impact : upstream.impact,\n severity: prefer === 'current' ? current.severity : upstream.severity,\n\n // Collections\n tags: mergeTags(current.tags, upstream.tags, prefer),\n descriptions: mergeDescriptions(current.descriptions, upstream.descriptions, prefer),\n refs: mergeRefs(current.refs, upstream.refs, prefer),\n\n // Code: current by default, upstream only with --prefer upstream\n code: prefer === 'upstream'\n ? upstream.code\n : (current.code ?? upstream.code),\n\n // SourceLocation follows scalars\n sourceLocation: prefer === 'current' ? current.sourceLocation : upstream.sourceLocation,\n };\n\n // Clean up undefined optional fields\n if (merged.code === undefined) delete merged.code;\n if (merged.severity === undefined) delete merged.severity;\n if (merged.sourceLocation === undefined) delete merged.sourceLocation;\n if (merged.refs === undefined || merged.refs.length === 0) delete merged.refs;\n\n return merged;\n}\n\n/**\n * Merge two tag maps.\n *\n * Default: union of keys; upstream wins on key conflicts.\n * prefer \"current\": union; current wins on key conflicts.\n * prefer \"upstream\": upstream replaces all.\n */\nexport function mergeTags(\n current: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any\n upstream: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any\n prefer?: PreferSide,\n): Record<string, any> { // eslint-disable-line @typescript-eslint/no-explicit-any\n if (prefer === 'upstream') {\n return { ...upstream };\n }\n\n const merged: Record<string, any> = { ...current }; // eslint-disable-line @typescript-eslint/no-explicit-any\n\n for (const [key, value] of Object.entries(upstream)) {\n if (prefer === 'current') {\n if (!(key in merged)) {\n merged[key] = value;\n }\n } else {\n // Default: upstream wins on conflict\n merged[key] = value;\n }\n }\n\n return merged;\n}\n\n/**\n * Merge two description arrays by label.\n *\n * Default: union by label; upstream wins on label conflicts.\n * prefer \"current\": union; current wins on label conflicts.\n * prefer \"upstream\": upstream replaces all.\n */\nexport function mergeDescriptions(\n current: Description[],\n upstream: Description[],\n prefer?: PreferSide,\n): Description[] {\n if (prefer === 'upstream') {\n return [...upstream];\n }\n\n const byLabel = new Map<string, Description>();\n const order: string[] = [];\n\n // Start with current\n for (const d of current) {\n byLabel.set(d.label, d);\n order.push(d.label);\n }\n\n // Apply upstream\n for (const d of upstream) {\n if (byLabel.has(d.label)) {\n if (prefer !== 'current') {\n // Default: upstream wins\n byLabel.set(d.label, d);\n }\n } else {\n byLabel.set(d.label, d);\n order.push(d.label);\n }\n }\n\n return order.map(label => byLabel.get(label)!);\n}\n\n/**\n * Merge two reference arrays.\n *\n * Default: union, deduplicated by JSON key.\n * prefer \"current\": current only.\n * prefer \"upstream\": upstream only.\n */\nexport function mergeRefs(\n current?: Reference[],\n upstream?: Reference[],\n prefer?: PreferSide,\n): Reference[] | undefined {\n if (prefer === 'current') {\n return current ? [...current] : undefined;\n }\n if (prefer === 'upstream') {\n return upstream ? [...upstream] : undefined;\n }\n\n // Default: union, deduplicated\n if (!current && !upstream) return undefined;\n\n const seen = new Set<string>();\n const result: Reference[] = [];\n\n const addRef = (r: Reference): void => {\n const key = JSON.stringify(r);\n if (!seen.has(key)) {\n seen.add(key);\n result.push(r);\n }\n };\n\n if (current) current.forEach(addRef);\n if (upstream) upstream.forEach(addRef);\n\n return result.length > 0 ? result : undefined;\n}\n","import type { HdfBaseline, BaselineRequirement } from '@mitre/hdf-schema';\nimport type { UpgradeResult, UpgradeOptions, DeltaResult, DeltaOptions, LinkRecord, DeltaStatistics } from './delta-types.js';\nimport type { InSpecProfile } from './types.js';\nimport { mergeRequirement } from './merge.js';\nimport { generateControlStub } from './control-stub.js';\nimport { generateInSpecYml } from './inspec-yml.js';\n\n/**\n * Generate an upgraded HDF Baseline by smart-merging current and upstream requirements.\n *\n * For each upstream requirement:\n * - If matched: smart-merge current + upstream fields per mergeRequirement semantics\n * - If unmatched: include upstream requirement as-is (new control)\n *\n * Current requirements with no upstream match are dropped by default\n * (a control removed from upstream should not survive the upgrade).\n * Set opts.keepUnmatched to retain them instead.\n */\nexport function generateUpgrade(\n currentBaseline: HdfBaseline,\n upstreamBaseline: HdfBaseline,\n linkRecords: LinkRecord[],\n opts?: UpgradeOptions,\n): UpgradeResult {\n const prefer = opts?.prefer;\n\n // Build lookup: newId -> LinkRecord (prefer primary over related)\n const linkByNewId = new Map<string, LinkRecord>();\n for (const lr of linkRecords) {\n const existing = linkByNewId.get(lr.newId);\n if (!existing || lr.relationship === 'primary') {\n linkByNewId.set(lr.newId, lr);\n }\n }\n\n // Build lookup: oldId -> current requirement\n const currentById = new Map<string, BaselineRequirement>();\n for (const req of currentBaseline.requirements) {\n currentById.set(req.id, req);\n }\n\n // Track which current IDs got matched\n const matchedCurrentIds = new Set<string>();\n\n // Merge upstream requirements\n const mergedReqs: BaselineRequirement[] = [];\n for (const upReq of upstreamBaseline.requirements) {\n const link = linkByNewId.get(upReq.id);\n if (link && link.oldId) {\n const curReq = currentById.get(link.oldId);\n if (curReq) {\n matchedCurrentIds.add(link.oldId);\n\n // Handle --noCode\n const effectiveCurrent = opts?.noCode\n ? { ...curReq, code: undefined }\n : curReq;\n\n mergedReqs.push(mergeRequirement(effectiveCurrent, upReq, prefer));\n continue;\n }\n }\n // Unmatched upstream: include as-is\n mergedReqs.push(upReq);\n }\n\n // Include unmatched current requirements only when explicitly opted in.\n // By default they're dropped — a control removed from upstream should\n // not survive the upgrade, matching SAF CLI delta semantics.\n if (opts?.keepUnmatched) {\n for (const curReq of currentBaseline.requirements) {\n if (!matchedCurrentIds.has(curReq.id)) {\n mergedReqs.push(curReq);\n }\n }\n }\n\n // Build upgraded baseline (use upstream metadata)\n const upgradedBaseline: HdfBaseline = {\n ...upstreamBaseline,\n requirements: mergedReqs,\n };\n\n // Compute statistics\n const statistics = computeStatistics(linkRecords, currentBaseline.requirements.length, upstreamBaseline.requirements.length);\n\n const result: UpgradeResult = {\n baseline: upgradedBaseline,\n linkRecords,\n statistics,\n };\n\n // Generate InSpec profile if requested\n const outputFormat = opts?.outputFormat ?? '';\n if (outputFormat === 'inspec' || outputFormat === 'both' || outputFormat === '') {\n result.profile = generateProfileFromBaseline(upgradedBaseline, opts);\n }\n\n return result;\n}\n\n/**\n * Legacy entry point. Wraps generateUpgrade for backward compatibility.\n *\n * @deprecated Use generateUpgrade instead.\n */\nexport function generateDelta(\n newBaseline: HdfBaseline,\n linkRecords: LinkRecord[],\n oldCodeMap: Map<string, string>,\n opts?: DeltaOptions,\n oldControlCount?: number,\n): DeltaResult {\n // Build synthetic current baseline from code map\n const currentReqs = buildCurrentReqsFromCodeMap(linkRecords, oldCodeMap);\n const currentBaseline: HdfBaseline = {\n name: 'current',\n requirements: currentReqs,\n groups: [],\n supports: [],\n };\n\n const result = generateUpgrade(currentBaseline, newBaseline, linkRecords, opts);\n\n // Patch statistics to use provided old control count\n result.statistics.oldControlsLength = oldControlCount ?? 0;\n\n return result;\n}\n\nfunction buildCurrentReqsFromCodeMap(\n linkRecords: LinkRecord[],\n oldCodeMap: Map<string, string>,\n): BaselineRequirement[] {\n const seen = new Set<string>();\n const reqs: BaselineRequirement[] = [];\n\n for (const lr of linkRecords) {\n if (!lr.oldId || seen.has(lr.oldId)) continue;\n seen.add(lr.oldId);\n\n const req: BaselineRequirement = {\n id: lr.oldId,\n impact: 0,\n tags: {},\n descriptions: [{ label: 'default', data: '' }],\n };\n const code = oldCodeMap.get(lr.oldId);\n if (code) {\n req.code = code;\n }\n reqs.push(req);\n }\n return reqs;\n}\n\nfunction generateProfileFromBaseline(baseline: HdfBaseline, opts?: UpgradeOptions): InSpecProfile {\n const controls = new Map<string, string>();\n const allStubs: string[] = [];\n\n for (const req of baseline.requirements) {\n const ruby = generateControlStub(req);\n\n if (opts?.singleFile) {\n allStubs.push(ruby);\n } else {\n const safeId = req.id.replace(/\\.\\./g, '').replace(/[/\\\\]/g, '') || 'unknown';\n controls.set(`controls/${safeId}.rb`, ruby);\n }\n }\n\n if (opts?.singleFile && allStubs.length > 0) {\n controls.set('controls/controls.rb', allStubs.join('\\n'));\n }\n\n const inspecYml = generateInSpecYml(baseline, {\n singleFile: opts?.singleFile,\n metadata: opts?.metadata,\n inspecVersion: opts?.inspecVersion,\n });\n\n return { inspecYml, controls };\n}\n\nfunction computeStatistics(linkRecords: LinkRecord[], oldControlCount: number, newControlCount: number): DeltaStatistics {\n let match = 0;\n let posMisMatch = 0;\n let dupMatch = 0;\n let noMatch = 0;\n\n for (const lr of linkRecords) {\n if (lr.relationship === 'related') {\n dupMatch++;\n } else if (lr.relationship === 'no-match') {\n noMatch++;\n } else if (lr.potentialMismatch) {\n posMisMatch++;\n } else {\n match++;\n }\n }\n\n return {\n oldControlsLength: oldControlCount,\n newControlsLength: newControlCount,\n totalMappedControls: match + posMisMatch + dupMatch,\n match,\n posMisMatch,\n dupMatch,\n noMatch,\n };\n}\n","import type { DeltaResult, LinkRecord } from './delta-types.js';\n\n/**\n * JSON report payload for delta operations.\n * SAF CLI-compatible: { links: LinkRecord[] }\n */\nexport interface DeltaJsonReport {\n links: LinkRecord[];\n}\n\n/**\n * Generate a structured JSON report from a delta result.\n * Matches SAF CLI's delta.json format: { ...diff, links }.\n */\nexport function generateDeltaJson(result: DeltaResult): DeltaJsonReport {\n return {\n links: result.linkRecords,\n };\n}\n\n/**\n * Format a per-link match method description matching SAF CLI's logMatchMethod.\n */\nfunction formatMatchMethod(lr: LinkRecord): string {\n const confidencePct = (lr.confidence * 100).toFixed(0) + '%';\n switch (lr.matchMethod) {\n case 'srgDeterministic':\n return `SRG deterministic (${lr.srg ?? '?'}) [${lr.relationship}]`;\n case 'srgCciTiebreak':\n return `SRG block + CCI tiebreak (Jaccard=${confidencePct}) [${lr.relationship}]`;\n case 'vendorFuzzyTitle':\n return `Vendor fuzzy title (confidence=${confidencePct}) [${lr.relationship}]`;\n case 'exactId':\n return `Exact ID [${lr.relationship}]`;\n case 'cciMatch':\n return `CCI match [${lr.relationship}]`;\n case 'fuzzyTitle':\n return `Fuzzy title (confidence=${confidencePct}) [${lr.relationship}]`;\n case 'none':\n return 'No match';\n default:\n return `${lr.matchMethod} [${lr.relationship}]`;\n }\n}\n\n/**\n * Generate a Markdown report from a delta result.\n * Matches SAF CLI's delta.md format: mapping table, control counts,\n * match statistics, and statistics validation.\n */\nexport function generateDeltaMarkdown(result: DeltaResult): string {\n const lines: string[] = [];\n const stats = result.statistics;\n\n // Mapping results — SAF CLI format: Old Control -> New Control\n if (result.linkRecords.length > 0) {\n lines.push('Mapping Results ===========================================================================');\n lines.push('\\tOld Control -> New Control');\n for (const lr of result.linkRecords) {\n if (lr.relationship !== 'no-match' && lr.oldId) {\n lines.push(`\\t ${lr.oldId} -> ${lr.newId}`);\n }\n }\n\n const totalMapped = stats.totalMappedControls;\n lines.push(`Total Mapped Controls: ${totalMapped}`);\n lines.push('');\n }\n\n // Control counts\n lines.push('Control Counts ===========================');\n lines.push(`Total Controls Available for Delta: ${stats.oldControlsLength}`);\n lines.push(` Total Controls Found on XCCDF: ${stats.newControlsLength}`);\n lines.push('');\n\n // Match statistics\n lines.push('Match Statistics =========================');\n lines.push(` Match Controls: ${stats.match}`);\n lines.push(` Possible Mismatch Controls: ${stats.posMisMatch}`);\n lines.push(` Related Match Controls: ${stats.dupMatch}`);\n lines.push(` No Match Controls: ${stats.noMatch}`);\n lines.push('');\n\n // Statistics validation\n lines.push('Statistics Validation =============================================');\n const totalMapped = stats.totalMappedControls;\n const matchMappedValid = (stats.match + stats.posMisMatch + stats.dupMatch) === totalMapped;\n const processedValid = (totalMapped + stats.noMatch) === stats.newControlsLength;\n lines.push(`Match + Mismatch + Related = Total Mapped Controls: (${stats.match}+${stats.posMisMatch}+${stats.dupMatch}=${totalMapped}) ${matchMappedValid}`);\n lines.push(` Total Processed = Total XCCDF Controls: (${totalMapped}+${stats.noMatch}=${totalMapped + stats.noMatch}) ${processedValid}`);\n lines.push('');\n\n // Per-control match method details\n if (result.linkRecords.length > 0) {\n lines.push('Match Details =============================================================');\n for (const lr of result.linkRecords) {\n if (lr.oldId) {\n lines.push(` ${lr.oldId} --> ${lr.newId}`);\n lines.push(` Match method: ${formatMatchMethod(lr)}`);\n } else {\n lines.push(` (none) --> ${lr.newId} [no match]`);\n }\n }\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mitre/hdf-generators",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Generate InSpec profile stubs from HDF Baseline definitions",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -25,7 +25,7 @@
25
25
  "author": "MITRE Corporation",
26
26
  "license": "Apache-2.0",
27
27
  "dependencies": {
28
- "@mitre/hdf-schema": "^3.1.0"
28
+ "@mitre/hdf-schema": "^3.2.0"
29
29
  },
30
30
  "engines": {
31
31
  "node": ">=22.0.0"