From 430ccf9324c85407c5db2b3036884fd343b70a31 Mon Sep 17 00:00:00 2001 From: Rajendra Shah Date: Sun, 1 Nov 2026 13:31:21 +0100 Subject: [PATCH] Fix HTML report reference group sorting Use numeric-aware collation with a deterministic tie-breaker for mixed reference IDs. Regenerate the bundled report resources and cover all grouping modes with optional Node.js regression tests. Fixes #2478 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/report/CMakeLists.txt | 12 ++++- tests/report/sort_groups.js | 77 +++++++++++++++++++++++++++++++++ xsl/xccdf-resources.xsl | 2 +- xsl/xccdf-resources/openscap.js | 18 +------- 4 files changed, 91 insertions(+), 18 deletions(-) create mode 100644 tests/report/sort_groups.js diff --git a/tests/report/CMakeLists.txt b/tests/report/CMakeLists.txt index de4b5f7315..3ba1c71356 100644 --- a/tests/report/CMakeLists.txt +++ b/tests/report/CMakeLists.txt @@ -1 +1,11 @@ -add_oscap_test("report.sh") \ No newline at end of file +add_oscap_test("report.sh") + +find_program(NODE_EXECUTABLE NAMES node nodejs) +if(NODE_EXECUTABLE) + add_oscap_ctest("report/sort_groups" + COMMAND ${NODE_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/sort_groups.js + LABELS "javascript" + ) +else() + message(STATUS "Node.js not found; skipping report/sort_groups test") +endif() \ No newline at end of file diff --git a/tests/report/sort_groups.js b/tests/report/sort_groups.js new file mode 100644 index 0000000000..57d6fe63bf --- /dev/null +++ b/tests/report/sort_groups.js @@ -0,0 +1,77 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); + +const xslDir = path.resolve(__dirname, "../../xsl"); +const source = fs.readFileSync(path.join(xslDir, "xccdf-resources/openscap.js"), "utf8"); +const bundle = fs.readFileSync(path.join(xslDir, "xccdf-resources.xsl"), "utf8"); +const start = bundle.indexOf("function openRuleDetailsDialog("); +const end = bundle.indexOf("]]>", start); +assert(start >= 0 && end > start, "Report JavaScript must be present in the generated bundle"); + +function permutations(values) { + if (values.length === 0) + return [[]]; + return values.flatMap((value, index) => + permutations(values.filter((_, other) => other !== index)) + .map(rest => [value, ...rest])); +} + +for (const [name, script] of [ + ["source", source], + ["generated bundle", bundle.slice(start, end)] +]) { + // Loading the report's sorting functions does not require running DOM setup. + const context = vm.createContext({ + document: {}, + $: () => ({ready: () => {}}) + }); + vm.runInContext(script, context); + + function check(input, expected, key = "cis") { + assert.deepStrictEqual( + Array.from(context.sortGroups([...input], key)), + expected, + `${name}: ${key} sorting of ${JSON.stringify(input)}` + ); + } + + const referenceKeys = [ + "cui", "nist", "anssi", "pcidss4", "nist-csf", "bsi", "ccn", "cis", + "cis-csc", "ism", "disa", "app-srg-ctr", "app-srg", "os-srg", "stigid", + "stigref_vulnerability_id", "stigref", "cjis", "hipaa", "isa-62443-2013", + "isa-62443-2009", "cobit5", "iso27001-2013", "nerc-cip", "ospp", + "pcidss", "dcid" + ]; + const mixed = ["1.2.1.1", "1.2.1.2", "enable_authselect", "reload_dconf_db"]; + for (const key of referenceKeys) { + for (const input of permutations(mixed)) + check(input, mixed, key); + } + + for (const input of permutations(["AU-9(3)", "AU-12 b", "AU-12(c)"])) + check(input, ["AU-9(3)", "AU-12 b", "AU-12(c)"], "nist"); + for (const input of permutations(["1", "01", "2", "10"])) + check(input, ["01", "1", "2", "10"]); + + check(["1.2.10", "1.2.2", "1.2", "1.10.1", "1.2.1"], + ["1.2", "1.2.1", "1.2.2", "1.2.10", "1.10.1"]); + check(["AC-2(10)", "AC-2(2)", "AC-2", "AC-1"], + ["AC-1", "AC-2", "AC-2(2)", "AC-2(10)"], "nist"); + check(["unknown", "1.2.1.2", "1.2.1.1"], ["1.2.1.1", "1.2.1.2", "unknown"]); + check([], []); + check(["1.2.1.2"], ["1.2.1.2"]); + check(["1.2.1.2", "1.2.1.2"], ["1.2.1.2", "1.2.1.2"]); + for (const input of permutations(["low", "high", "medium"])) + check(input, ["high", "medium", "low"], context.KeysEnum.SEVERITY); + for (const input of permutations(["pass", "notapplicable", "fail"])) + check(input, ["fail", "notapplicable", "pass"], context.KeysEnum.RESULT); + + let resets = 0; + context.resetTreetable = () => { resets++; }; + context.$ = () => { throw new Error("Default grouping must not rebuild the table"); }; + context.groupRulesBy(context.KeysEnum.DEFAULT); + assert.strictEqual(resets, 1, `${name}: default grouping restores the original table`); + console.log(`${name}: report group sorting tests passed`); +} diff --git a/xsl/xccdf-resources.xsl b/xsl/xccdf-resources.xsl index 7f7cf5eda2..927ef4367a 100644 --- a/xsl/xccdf-resources.xsl +++ b/xsl/xccdf-resources.xsl @@ -34,5 +34,5 @@ padding:"inner"+a,content:b,"":"outer"+a},function(c,d){n.fn[d]=function(d,e){va * Generated using the Bootstrap Customizer (https://getbootstrap.com/customize/?id=8160adef040364fa8f688f6065765caf) * Config saved to config.json and https://gist.github.com/8160adef040364fa8f688f6065765caf */ -;if("undefined"==typeof jQuery){throw new Error("Bootstrap's JavaScript requires jQuery")}+function(a){var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1||b[0]>3){throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4")}}(jQuery),+function(b){function c(g){return this.each(function(){var e=b(this),h=e.data("bs.alert");h||e.data("bs.alert",h=new f(this)),"string"==typeof g&&h[g].call(e)})}var a='[data-dismiss="alert"]',f=function(g){b(g).on("click",a,this.close)};f.VERSION="3.3.7",f.TRANSITION_DURATION=150,f.prototype.close=function(k){function h(){g.detach().trigger("closed.bs.alert").remove()}var l=b(this),j=l.attr("data-target");j||(j=l.attr("href"),j=j&&j.replace(/.*(?=#[^\s]*$)/,""));var g=b("#"===j?[]:j);k&&k.preventDefault(),g.length||(g=l.closest(".alert")),g.trigger(k=b.Event("close.bs.alert")),k.isDefaultPrevented()||(g.removeClass("in"),b.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",h).emulateTransitionEnd(f.TRANSITION_DURATION):h())};var d=b.fn.alert;b.fn.alert=c,b.fn.alert.Constructor=f,b.fn.alert.noConflict=function(){return b.fn.alert=d,this},b(document).on("click.bs.alert.data-api",a,f.prototype.close)}(jQuery),+function(d){function h(l){var a=l.attr("data-target");a||(a=l.attr("href"),a=a&&/#[A-Za-z]/.test(a)&&a.replace(/.*(?=#[^\s]*$)/,""));var m=a&&d(a);return m&&m.length?m:l.parent()}function c(a){a&&3===a.which||(d(j).remove(),d(f).each(function(){var m=d(this),l=h(m),e={relatedTarget:this};l.hasClass("open")&&(a&&"click"==a.type&&/input|textarea/i.test(a.target.tagName)&&d.contains(l[0],a.target)||(l.trigger(a=d.Event("hide.bs.dropdown",e)),a.isDefaultPrevented()||(m.attr("aria-expanded","false"),l.removeClass("open").trigger(d.Event("hidden.bs.dropdown",e)))))}))}function k(a){return this.each(function(){var e=d(this),l=e.data("bs.dropdown");l||e.data("bs.dropdown",l=new b(this)),"string"==typeof a&&l[a].call(e)})}var j=".dropdown-backdrop",f='[data-toggle="dropdown"]',b=function(a){d(a).on("click.bs.dropdown",this.toggle)};b.VERSION="3.3.7",b.prototype.toggle=function(q){var p=d(this);if(!p.is(".disabled, :disabled")){var l=h(p),e=l.hasClass("open");if(c(),!e){"ontouchstart" in document.documentElement&&!l.closest(".navbar-nav").length&&d(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(d(this)).on("click",c);var m={relatedTarget:this};if(l.trigger(q=d.Event("show.bs.dropdown",m)),q.isDefaultPrevented()){return}p.trigger("focus").attr("aria-expanded","true"),l.toggleClass("open").trigger(d.Event("shown.bs.dropdown",m))}return !1}},b.prototype.keydown=function(p){if(/(38|40|27|32)/.test(p.which)&&!/input|textarea/i.test(p.target.tagName)){var u=d(this);if(p.preventDefault(),p.stopPropagation(),!u.is(".disabled, :disabled")){var t=h(u),m=t.hasClass("open");if(!m&&27!=p.which||m&&27==p.which){return 27==p.which&&t.find(f).trigger("focus"),u.trigger("click")}var q=" li:not(.disabled):visible a",s=t.find(".dropdown-menu"+q);if(s.length){var e=s.index(p.target);38==p.which&&e>0&&e--,40==p.which&&edocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&e?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!e?this.scrollbarWidth:""})},a.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},a.prototype.checkScrollbar=function(){var f=window.innerWidth;if(!f){var g=document.documentElement.getBoundingClientRect();f=g.right-Math.abs(g.left)}this.bodyIsOverflowing=document.body.clientWidth❌');var b=$('');$("body").prepend(b);var c=$("#rule-detail-"+d).clone();c.attr("id","");c.children(".panel-heading").append(a);a.css({"float":"right"});a.css({"margin-top":"-=23px"});$("#detail-modal-body").append(c);$("#detail-modal").on("hidden.bs.modal",function(f){$("#detail-modal").remove()});$("#detail-modal").modal();return false}function toggleRuleDisplay(b){var a=b.value;if(b.checked){$(".rule-overview-leaf-"+a).removeClass("rule-result-filtered");$(".rule-detail-"+a).removeClass("rule-result-filtered")}else{$(".rule-overview-leaf-"+a).addClass("rule-result-filtered");$(".rule-detail-"+a).addClass("rule-result-filtered")}stripeTreeTable()}function toggleResultDetails(b){var a=$("#result-details");if(a.is(":visible")){a.hide();$(b).html("Show all result details")}else{a.show();$(b).html("Hide all result details")}return false}function ruleSearchMatches(e,c){if(c.length==0){return true}var b=true;var d=e.children(".keywords").text().toLowerCase();var a;for(a=0;a0){$("#search-matches").html(b.toString()+" rules match.")}else{$("#search-matches").html("No rules match your search criteria!")}}}var is_original=true;var original_treetable=null;$(document).ready(function(){$("#result-details").hide();$(".js-only").show();$(".form-group select").val("default");$(".toggle-rule-display").each(function(){toggleRuleDisplay(this)});original_treetable=$(".treetable").clone();$(".treetable").treetable({column:0,expandable:true,clickableNodeNames:true,initialState:"expanded",indent:0});is_original=true;stripeTreeTable()});function resetTreetable(){if(!is_original){$(".treetable").remove();$("#rule-overview").append(original_treetable.clone());$(".treetable").treetable({column:0,expandable:true,clickableNodeNames:true,initialState:"expanded",indent:0});$(".toggle-rule-display").each(function(){toggleRuleDisplay(this)});is_original=true}}function newGroupLine(a,c){var b=24;if(a.length>b){a=a.substring(0,b-1)+"…"}return''+a+" = "+c+""}var KeysEnum={DEFAULT:"default",SEVERITY:"severity",RESULT:"result",NIST:"NIST SP 800-53 ID",DISA_CCI:"DISA CCI",DISA_SRG:"DISA SRG",DISA_STIG_ID:"DISA STIG ID",PCI_DSS:"PCI DSS Requirement",CIS:"CIS Recommendation"};function getTargetGroupsList(f,d){switch(d){case KeysEnum.SEVERITY:var b=f.children(".rule-severity").text();return[b];case KeysEnum.RESULT:var a=f.children(".rule-result").text();return[a];default:try{var c=JSON.parse(f.attr("data-references"))}catch(e){return["unknown"]}if(!c.hasOwnProperty(d)){return["unknown"]}return c[d]}}function sortGroups(a,b){switch(b){case KeysEnum.SEVERITY:return["high","medium","low"];case KeysEnum.RESULT:return a.sort();default:return a.sort(function(e,d){var f=e.split(/[.()-]/);var g=d.split(/[.()-]/);var c=0;var j=Math.min(f.length,g.length);var h=/^[1-9][0-9]*$/;for(i=0;i").parent().html();b[e].push(f)}});$(".treetable").remove();var a=sortGroups(Object.keys(b),c);var d="";for(i=0;iGroup SeverityResult'+d+"";$("#rule-overview").append(new_table);is_original=false;$(".treetable").treetable({column:0,expandable:true,clickableNodeNames:true,initialState:"expanded",indent:0});stripeTreeTable()}function stripeTreeTable(){var a=$(".rule-overview-leaf:not(.rule-result-filtered)");var b=false;$(a).each(function(){$(this).css("background-color",b?"#F9F9F9":"inherit");b=!b})};]]> +;if("undefined"==typeof jQuery){throw new Error("Bootstrap's JavaScript requires jQuery")}+function(a){var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1||b[0]>3){throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4")}}(jQuery),+function(b){function c(g){return this.each(function(){var e=b(this),h=e.data("bs.alert");h||e.data("bs.alert",h=new f(this)),"string"==typeof g&&h[g].call(e)})}var a='[data-dismiss="alert"]',f=function(g){b(g).on("click",a,this.close)};f.VERSION="3.3.7",f.TRANSITION_DURATION=150,f.prototype.close=function(k){function h(){g.detach().trigger("closed.bs.alert").remove()}var l=b(this),j=l.attr("data-target");j||(j=l.attr("href"),j=j&&j.replace(/.*(?=#[^\s]*$)/,""));var g=b("#"===j?[]:j);k&&k.preventDefault(),g.length||(g=l.closest(".alert")),g.trigger(k=b.Event("close.bs.alert")),k.isDefaultPrevented()||(g.removeClass("in"),b.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",h).emulateTransitionEnd(f.TRANSITION_DURATION):h())};var d=b.fn.alert;b.fn.alert=c,b.fn.alert.Constructor=f,b.fn.alert.noConflict=function(){return b.fn.alert=d,this},b(document).on("click.bs.alert.data-api",a,f.prototype.close)}(jQuery),+function(d){function h(l){var a=l.attr("data-target");a||(a=l.attr("href"),a=a&&/#[A-Za-z]/.test(a)&&a.replace(/.*(?=#[^\s]*$)/,""));var m=a&&d(a);return m&&m.length?m:l.parent()}function c(a){a&&3===a.which||(d(j).remove(),d(f).each(function(){var m=d(this),l=h(m),e={relatedTarget:this};l.hasClass("open")&&(a&&"click"==a.type&&/input|textarea/i.test(a.target.tagName)&&d.contains(l[0],a.target)||(l.trigger(a=d.Event("hide.bs.dropdown",e)),a.isDefaultPrevented()||(m.attr("aria-expanded","false"),l.removeClass("open").trigger(d.Event("hidden.bs.dropdown",e)))))}))}function k(a){return this.each(function(){var e=d(this),l=e.data("bs.dropdown");l||e.data("bs.dropdown",l=new b(this)),"string"==typeof a&&l[a].call(e)})}var j=".dropdown-backdrop",f='[data-toggle="dropdown"]',b=function(a){d(a).on("click.bs.dropdown",this.toggle)};b.VERSION="3.3.7",b.prototype.toggle=function(q){var p=d(this);if(!p.is(".disabled, :disabled")){var l=h(p),e=l.hasClass("open");if(c(),!e){"ontouchstart" in document.documentElement&&!l.closest(".navbar-nav").length&&d(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(d(this)).on("click",c);var m={relatedTarget:this};if(l.trigger(q=d.Event("show.bs.dropdown",m)),q.isDefaultPrevented()){return}p.trigger("focus").attr("aria-expanded","true"),l.toggleClass("open").trigger(d.Event("shown.bs.dropdown",m))}return !1}},b.prototype.keydown=function(p){if(/(38|40|27|32)/.test(p.which)&&!/input|textarea/i.test(p.target.tagName)){var u=d(this);if(p.preventDefault(),p.stopPropagation(),!u.is(".disabled, :disabled")){var t=h(u),m=t.hasClass("open");if(!m&&27!=p.which||m&&27==p.which){return 27==p.which&&t.find(f).trigger("focus"),u.trigger("click")}var q=" li:not(.disabled):visible a",s=t.find(".dropdown-menu"+q);if(s.length){var e=s.index(p.target);38==p.which&&e>0&&e--,40==p.which&&edocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&e?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!e?this.scrollbarWidth:""})},a.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},a.prototype.checkScrollbar=function(){var f=window.innerWidth;if(!f){var g=document.documentElement.getBoundingClientRect();f=g.right-Math.abs(g.left)}this.bodyIsOverflowing=document.body.clientWidth❌');var b=$('');$("body").prepend(b);var c=$("#rule-detail-"+d).clone();c.attr("id","");c.children(".panel-heading").append(a);a.css({"float":"right"});a.css({"margin-top":"-=23px"});$("#detail-modal-body").append(c);$("#detail-modal").on("hidden.bs.modal",function(f){$("#detail-modal").remove()});$("#detail-modal").modal();return false}function toggleRuleDisplay(b){var a=b.value;if(b.checked){$(".rule-overview-leaf-"+a).removeClass("rule-result-filtered");$(".rule-detail-"+a).removeClass("rule-result-filtered")}else{$(".rule-overview-leaf-"+a).addClass("rule-result-filtered");$(".rule-detail-"+a).addClass("rule-result-filtered")}stripeTreeTable()}function toggleResultDetails(b){var a=$("#result-details");if(a.is(":visible")){a.hide();$(b).html("Show all result details")}else{a.show();$(b).html("Hide all result details")}return false}function ruleSearchMatches(e,c){if(c.length==0){return true}var b=true;var d=e.children(".keywords").text().toLowerCase();var a;for(a=0;a0){$("#search-matches").html(b.toString()+" rules match.")}else{$("#search-matches").html("No rules match your search criteria!")}}}var is_original=true;var original_treetable=null;$(document).ready(function(){$("#result-details").hide();$(".js-only").show();$(".form-group select").val("default");$(".toggle-rule-display").each(function(){toggleRuleDisplay(this)});original_treetable=$(".treetable").clone();$(".treetable").treetable({column:0,expandable:true,clickableNodeNames:true,initialState:"expanded",indent:0});is_original=true;stripeTreeTable()});function resetTreetable(){if(!is_original){$(".treetable").remove();$("#rule-overview").append(original_treetable.clone());$(".treetable").treetable({column:0,expandable:true,clickableNodeNames:true,initialState:"expanded",indent:0});$(".toggle-rule-display").each(function(){toggleRuleDisplay(this)});is_original=true}}function newGroupLine(a,c){var b=24;if(a.length>b){a=a.substring(0,b-1)+"…"}return''+a+" = "+c+""}var KeysEnum={DEFAULT:"default",SEVERITY:"severity",RESULT:"result",NIST:"NIST SP 800-53 ID",DISA_CCI:"DISA CCI",DISA_SRG:"DISA SRG",DISA_STIG_ID:"DISA STIG ID",PCI_DSS:"PCI DSS Requirement",CIS:"CIS Recommendation"};function getTargetGroupsList(f,d){switch(d){case KeysEnum.SEVERITY:var b=f.children(".rule-severity").text();return[b];case KeysEnum.RESULT:var a=f.children(".rule-result").text();return[a];default:try{var c=JSON.parse(f.attr("data-references"))}catch(e){return["unknown"]}if(!c.hasOwnProperty(d)){return["unknown"]}return c[d]}}function sortGroups(a,b){switch(b){case KeysEnum.SEVERITY:return["high","medium","low"];case KeysEnum.RESULT:return a.sort();default:return a.sort(function(d,c){return d.localeCompare(c,undefined,{numeric:true})||d.localeCompare(c)})}}function groupRulesBy(c){resetTreetable();if(c==KeysEnum.DEFAULT){return}var b={};$(".rule-overview-leaf").each(function(){$(this).children("td:first").css("padding-left","0px");var j=$(this).attr("data-tt-id");var g=getTargetGroupsList($(this),c);for(i=0;i").parent().html();b[e].push(f)}});$(".treetable").remove();var a=sortGroups(Object.keys(b),c);var d="";for(i=0;iGroup SeverityResult'+d+"";$("#rule-overview").append(new_table);is_original=false;$(".treetable").treetable({column:0,expandable:true,clickableNodeNames:true,initialState:"expanded",indent:0});stripeTreeTable()}function stripeTreeTable(){var a=$(".rule-overview-leaf:not(.rule-result-filtered)");var b=false;$(a).each(function(){$(this).css("background-color",b?"#F9F9F9":"inherit");b=!b})};]]> diff --git a/xsl/xccdf-resources/openscap.js b/xsl/xccdf-resources/openscap.js index 07c0df5158..f33ebca419 100644 --- a/xsl/xccdf-resources/openscap.js +++ b/xsl/xccdf-resources/openscap.js @@ -194,22 +194,8 @@ function sortGroups(groups, key) return groups.sort(); default: return groups.sort(function(a, b){ - var a_parts = a.split(/[.()-]/); - var b_parts = b.split(/[.()-]/); - var result = 0; - var min_length = Math.min(a_parts.length, b_parts.length); - var number = /^[1-9][0-9]*$/; - for (i = 0; i < min_length && result == 0; i++) { - if (a_parts[i].match(number) == null || a_parts[i].match(number) == null) { - result = a_parts[i].localeCompare(b_parts[i]); - } else { - result = parseInt(a_parts[i]) - parseInt(b_parts[i]); - } - } - if (result == 0) { - result = a_parts.length - b_parts.length; - } - return result; + // Break numeric ties consistently, including references with leading zeros. + return a.localeCompare(b, undefined, {numeric: true}) || a.localeCompare(b); }); } }