Skip to content

Commit

Permalink
Test merge phase, plus minor XSLT enhancements (#1207)
Browse files Browse the repository at this point in the history
XSLT
- Move some code from oscal-profile-resolve-select.xsl to new file, select-or-custom-merge.xsl, and share it between Selection phase and Merge phase.
- Add error checking for multiple structuring directives.
- Finish the incomplete support for `insert-controls/@order`.
- Fix bug in support for `@with-parent-controls` in Selection phase.

XSpec
- In select.xspec, add test for order of controls matching imported document, not profile document. Also, add test for `@with-parent-controls` bug fix.
- Create merge-combine.xspec for testing combine-elements and mode="o:combine-elements" templates.
- Organize tests among merge.xspec, merge-combine.xspec, merge-as-is.xspec, and merge-custom.xspec.
- Integrate back-matter testing into template-level tests in merge.xspec and merge-as-is.xspec, and delete merge-back-matter.xspec.
  • Loading branch information
galtm authored May 4, 2022
1 parent de13b23 commit 6587e35
Show file tree
Hide file tree
Showing 9 changed files with 2,364 additions and 662 deletions.
153 changes: 94 additions & 59 deletions src/utils/util/resolver-pipeline/oscal-profile-resolve-merge.xsl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0"
<xsl:stylesheet version="3.0"
xmlns="http://csrc.nist.gov/ns/oscal/1.0"
xmlns:mh="http://csrc.nist.gov/ns/message"
xmlns:o="http://csrc.nist.gov/ns/oscal/1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
Expand All @@ -9,8 +10,10 @@
exclude-result-prefixes="xs math o opr"
xpath-default-namespace="http://csrc.nist.gov/ns/oscal/1.0" >

<!-- XSLT 2.0 so as to validate against XSLT 3.0 constructs -->
<xsl:import href="message-handler.xsl"/>

<xsl:variable name="in_xspec" as="xs:boolean" select="false()"/>
<xsl:variable name="true-content" as="xs:string+" select="('true','1')"/>

<xsl:template match="* | @*" mode="#all">
<xsl:copy copy-namespaces="no">
Expand All @@ -25,34 +28,43 @@
templates not this one. -->
</xsl:template>

<xsl:template match="catalog" priority="2">
<!-- If there is no selection and no merge (i.e., we don't reach one of
the higher-priority templates that match catalog), pass the catalog
through unchanged. -->
<xsl:template match="catalog" priority="2" as="element(catalog)">
<xsl:copy-of select="."/>
</xsl:template>

<xsl:template match="catalog[exists(selection)]" priority="10">
<!-- If there is a selection but neither merge/as-is nor merge/custom,
use flat structuring. -->
<xsl:template match="catalog[exists(selection)]" priority="10" as="element(catalog)">
<catalog>
<xsl:apply-templates select="@*"/>
<xsl:apply-templates select="metadata"/>
<xsl:apply-templates select="selection"/>
<xsl:apply-templates select="modify"/>
<xsl:for-each-group select="back-matter/* | selection/back-matter/*" group-by="true()">
<xsl:where-populated>
<back-matter>
<xsl:copy-of select="current-group()"/>
<xsl:sequence select="back-matter/* | selection/back-matter/*"/>
</back-matter>
</xsl:for-each-group>
</xsl:where-populated>
</catalog>
</xsl:template>

<xsl:template priority="12" match="catalog[merge/as-is=('true','1')]">
<!-- If there is a merge/as-is directive, we go down that branch.
If there is also a merge/custom directive, we apply the
higher-priority template instead of this one. -->
<xsl:template match="catalog[merge/as-is=$true-content]" priority="12" as="element(catalog)">
<xsl:call-template name="detect-multiple-structuring-directives"/>
<catalog>
<xsl:apply-templates select="@*"/>
<xsl:apply-templates select="metadata"/>
<xsl:variable name="merged-selections">
<xsl:call-template name="o:merge-groups-as-is">
<xsl:with-param name="merging" select="selection"/>
</xsl:call-template>
<xsl:call-template name="o:merge-groups-as-is">
<xsl:with-param name="merging" select="selection"/>
</xsl:call-template>
</xsl:variable>
<!-- not copying the selection elements only their contents -->
<!-- not copying the selection elements, only their contents -->
<xsl:for-each select="$merged-selections/selection">
<xsl:sequence select="* except back-matter"/>
</xsl:for-each>
Expand All @@ -64,7 +76,8 @@


<!-- If there is a merge/custom directive, we go down that branch. -->
<xsl:template priority="13" match="catalog[exists(merge/custom)]">
<xsl:template match="catalog[exists(merge/custom)]" priority="13" as="element(catalog)">
<xsl:call-template name="detect-multiple-structuring-directives"/>
<catalog>
<xsl:apply-templates select="@*"/>
<xsl:apply-templates select="metadata"/>
Expand All @@ -77,83 +90,93 @@
</catalog>
</xsl:template>

<xsl:template name="combine-back-matter">
<xsl:variable name="here" select="self::catalog"/>
<xsl:for-each-group select="back-matter/* | selection/back-matter/*" group-by="true()">
<xsl:template name="detect-multiple-structuring-directives" as="empty-sequence()">
<xsl:context-item as="element(catalog)" use="required"/>
<xsl:variable name="flat" as="element(flat)*" select="merge/flat[.=$true-content]"/>
<xsl:variable name="as-is" as="element(as-is)*" select="merge/as-is[.=$true-content]"/>
<xsl:variable name="custom" as="element(custom)*" select="merge/custom"/>
<xsl:if test="count($flat) + count($as-is) + count($custom) gt 1">
<xsl:call-template name="mh:message-handler">
<xsl:with-param name="text">Found multiple structuring directives. Choose at most one from: flat (default), as-is, custom.</xsl:with-param>
<xsl:with-param name="message-type">Error</xsl:with-param>
<xsl:with-param name="terminate" select="true()"/>
</xsl:call-template>
</xsl:if>
</xsl:template>

<xsl:template name="combine-back-matter" as="element(back-matter)?">
<xsl:context-item as="element(catalog)" use="required"/>
<xsl:where-populated>
<back-matter>
<!-- Using combination logic on back matter elements. -->
<xsl:for-each-group select="current-group()" group-by="(@opr:id,@id,generate-id())[1]">
<xsl:for-each-group select="back-matter/* | selection/back-matter/*" group-by="(@opr:id,@uuid,generate-id())[1]">
<xsl:call-template name="combine-elements">
<xsl:with-param name="who" select="current-group()"/>
<!-- Take last one in group because of spec
requirement id="req-backmatter-dupe". -->
<xsl:with-param name="who" select="current-group()[last()]" as="element(resource)"/>
</xsl:call-template>
</xsl:for-each-group>
</back-matter>
</xsl:for-each-group>
</xsl:where-populated>
</xsl:template>

<xsl:template match="selection">
<xsl:apply-templates select="param | .//group/param"/>
<xsl:apply-templates select="control | .//group/control"/>
</xsl:template>

<xsl:key name="control-by-id" match="control" use="@id"/>

<xsl:template match="node() | @*" mode="o:custom-merge">
<xsl:copy>
<xsl:apply-templates mode="#current" select="node() | @*"/>
</xsl:copy>
</xsl:template>

<!-- Process children but do not copy <custom> tags themselves. -->
<xsl:template match="custom" mode="o:custom-merge">
<xsl:apply-templates mode="#current"/>
</xsl:template>

<!-- Apply the requested ordering.
Creating the properly combined sequence of inserted controls
is in downstream template. -->
<xsl:template match="insert-controls" mode="o:custom-merge">
<xsl:variable name="inserted-controls">
<xsl:variable name="inserted-controls" as="element()*">
<xsl:apply-templates mode="#current"/>
</xsl:variable>
<xsl:variable name="keep-order" select="not(@order = ('descending','ascending'))" as="xs:boolean"/>
<xsl:variable name="sort-order" select="@order[.='descending'],'ascending'"/>
<!-- Setting sort-key to '1' sorts into given order -->
<xsl:perform-sort select="$inserted-controls">
<xsl:sort select="if ($keep-order) then '1' else @control-id"/>
<xsl:perform-sort select="$inserted-controls">
<xsl:sort select="if ($keep-order) then '1' else @id"
order="{$sort-order[1]}"/>
</xsl:perform-sort>
</xsl:template>

<xsl:template match="include-controls" mode="o:custom-merge">
<xsl:variable name="match-patterns" select="matching/@pattern"/>
<xsl:call-template name="combine-elements">
<xsl:with-param name="who" select="key('control-by-id', with-id),
/*/selection//control[some $p in ($match-patterns) satisfies (matches(@id,o:glob-as-regex(string($p))))]"/>
</xsl:call-template>
</xsl:template>
<xsl:include href="select-or-custom-merge.xsl"/>

<xsl:template match="include-all" mode="o:custom-merge">
<xsl:variable name="match-patterns" select="matching/@pattern"/>
<!-- Combine selected controls.
Creating the sequence of controls ($who) is in code shared with
selection phase. -->
<xsl:template match="include-all | include-controls" mode="o:custom-merge">
<xsl:variable name="who" as="element(o:control)*">
<xsl:apply-templates select="ancestor::*[last()]//selection" mode="o:select">
<xsl:with-param tunnel="yes" name="import-instruction" select="ancestor::insert-controls"/>
</xsl:apply-templates>
</xsl:variable>
<xsl:call-template name="combine-elements">
<xsl:with-param name="who" select="//control"/>
<xsl:with-param name="who" select="$who"/>
</xsl:call-template>
</xsl:template>

<xsl:include href="oscal-profile-resolve-functions.xsl"/>
<!-- No-op because exclusion logic is handled in the code shared with
selection phase. -->
<xsl:template match="exclude-controls" mode="o:custom-merge"/>

<!--<xsl:template match="with-id">
<xsl:call-template name="combine-elements">
<xsl:with-param name="who" select="key('control-by-id', @control-id)"/>
</xsl:call-template>
<!-- In o:select mode, process children of selection or group to reach
controls, which are handled in select-or-custom-merge.xsl. -->
<xsl:template match="selection | selection//group" mode="o:select">
<xsl:apply-templates select="group | control" mode="#current"/>
</xsl:template>

<xsl:template match="matching" mode="o:custom-merge">
<xsl:variable name="p" select="@pattern"/>
<xsl:call-template name="combine-elements">
<xsl:with-param name="who" select="/*/selection//control[matches(@id,$p)]"/>
</xsl:call-template>
</xsl:template>-->

<xsl:template name="o:merge-groups-as-is">
<xsl:param name="merging" select="()"/>
<xsl:context-item as="element()" use="optional"/>
<xsl:param name="merging" select="()" as="element()*"/>
<xsl:for-each-group select="$merging" group-by="(@uuid,@opr:id,@id,generate-id())[1]">
<xsl:variable name="merged" select="current-group()"/>
<xsl:variable name="merged" select="current-group()" as="element()+"/>
<xsl:for-each select="$merged[1]">
<xsl:copy copy-namespaces="no">
<xsl:apply-templates select="$merged/@*"/>
Expand All @@ -177,6 +200,7 @@
</xsl:template>

<xsl:template name="combine-elements">
<xsl:context-item as="element()" use="required"/>
<xsl:param name="who" as="element()*"/>
<xsl:apply-templates select="ancestor-or-self::catalog" mode="o:combine-elements">
<xsl:with-param name="elements" select="$who"/>
Expand All @@ -194,15 +218,26 @@
<!-- All the elements coming in have the same name but different opr:id
we operate on controls and parameters (not groups or group contents otherwise) -->
<!-- further, we assume all controls or all parameters -->
<xsl:if test="$elements/name() != $elements/name()">
<opr:error> ... elements of different types combining ...</opr:error></xsl:if>
<xsl:variable name="uri-qualified-name" as="function(*)"
select="function($node as element()) as xs:string {
'Q{' || namespace-uri($node) || '}' || local-name($node)
}"/>
<xsl:variable name="element-names" as="xs:string*"
select="$elements ! $uri-qualified-name(.)"/>
<xsl:if test="$element-names != $element-names">
<!-- If we get here for a valid profile, it is an XSLT bug rather than a user error. -->
<xsl:call-template name="mh:message-handler">
<xsl:with-param name="text">Combining elements of different types is not supported.</xsl:with-param>
<xsl:with-param name="message-type">Error</xsl:with-param>
<xsl:with-param name="terminate" select="$in_xspec"/>
</xsl:call-template>
</xsl:if>
<xsl:for-each-group select="$elements" group-by="(@opr:id,@id,generate-id())[1]">
<xsl:variable name="merged" select="current-group()"/>
<xsl:variable name="merged" select="current-group()" as="element()+"/>
<xsl:for-each select="$merged[1]">
<xsl:copy copy-namespaces="no">
<xsl:apply-templates select="$merged/@*"/>
<xsl:apply-templates select="$merged/*"/>

</xsl:copy>
</xsl:for-each>
</xsl:for-each-group>
Expand All @@ -219,7 +254,7 @@
<xsl:apply-templates select="$elements"/>
</xsl:template>

<!-- Scrubbing opr:id values on the way out - we don't need them. -->
<!-- Scrubbing opr:id values on the way out - we don't need them. -->
<xsl:template match="@opr:id"/>

</xsl:stylesheet>
80 changes: 2 additions & 78 deletions src/utils/util/resolver-pipeline/oscal-profile-resolve-select.xsl
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
<xsl:variable name="linked-xml" select="child::rlink[ends-with(@href,'.xml') or matches(@media-type,'xml')][1]"/>
<xsl:choose>
<xsl:when test="exists($linked-xml)">
<xsl:apply-templates mode="o:select" select="o:resource-or-error($linked-xml/@href)"/>
<xsl:apply-templates mode="o:select" select="o:resource-or-error($linked-xml/@href)"/>
</xsl:when>
<xsl:otherwise>
<xsl:message terminate="yes"
Expand Down Expand Up @@ -126,30 +126,6 @@
</xsl:copy>
</xsl:template>

<xsl:template name="add-process-id" as="attribute(opr:id)">
<xsl:param name="context" select="." as="element()"/>
<xsl:attribute name="opr:id" namespace="http://csrc.nist.gov/ns/oscal/profile-resolution">
<xsl:value-of
select="concat(opr:catalog-identifier($context/root()/o:catalog), '#', $context/(@id, generate-id())[1])"/>
</xsl:attribute>
</xsl:template>

<xsl:function name="opr:catalog-identifier" as="xs:string">
<xsl:param name="catalog" as="element(o:catalog)"/>
<xsl:sequence select="$catalog/(@uuid,document-uri(root(.)))[1]"/>
</xsl:function>

<!-- A control is included if it is selected by the provided import instruction -->
<xsl:template match="control" mode="o:select" as="element(o:control)?">
<xsl:param name="import-instruction" tunnel="yes" required="yes"/>
<xsl:if test="o:selects($import-instruction,.)">
<xsl:copy copy-namespaces="no">
<xsl:call-template name="add-process-id"/>
<xsl:apply-templates mode="#current" select="node() | @*"/>
</xsl:copy>
</xsl:if>
</xsl:template>

<!-- Parameters are always passed through until later stages. -->
<xsl:template match="param" mode="o:select">
<xsl:copy copy-namespaces="no">
Expand All @@ -170,58 +146,6 @@
</xsl:if>
</xsl:template>-->

<!-- Function o:selects($importing,$candidate) returns a true or false
depending on whether the import calls the candidate control -->

<!-- @with-child-controls='yes' is recursive - given on a control it brings all control descendants, not only children -->
<xsl:function name="o:selects" as="xs:boolean">
<xsl:param name="importing" as="element(o:import)"/>
<xsl:param name="candidate" as="element(o:control)"/>
<xsl:variable name="include-reasons" as="xs:boolean+">
<!-- we are not optimizing for performance here; nothing is done to prevent all checks even if the first passes -->
<!--<xsl:sequence select="empty($importing/include)"/>-->
<xsl:sequence select="exists($importing/include-all)"/>
<xsl:sequence select="some $c in ($importing/include-controls/with-id)
satisfies ($c = $candidate/@id)"/>
<xsl:sequence select="some $c in ($importing/include-controls[o:calls-parents(.)]/with-id)
satisfies ($c = $candidate/descendant::control/@id)"/>
<xsl:sequence select="some $c in ($importing/include-controls[o:calls-children(.)]/with-id)
satisfies ($c = $candidate/ancestor::control/@id)"/>
<xsl:sequence select="some $m in ($importing/include-controls/matching[@pattern != ''])
satisfies (matches($candidate/@id,$m/@pattern/o:glob-as-regex(string(.)) ))"/>
<xsl:sequence select="some $m in ($importing/include-controls[o:calls-parents(.)]/matching[@pattern != '']), $a in $candidate/descendant::control
satisfies (matches($a/@id,$m/@pattern/o:glob-as-regex(string(.))))"/>
<xsl:sequence select="some $m in ($importing/include-controls[o:calls-children(.)]/matching[@pattern != '']), $a in $candidate/ancestor::control
satisfies (matches($a/@id,$m/@pattern/o:glob-as-regex(string(.))))"/>
</xsl:variable>
<xsl:variable name="exclude-reasons" as="xs:boolean+">
<xsl:sequence select="exists($candidate/parent::control) and $importing/include-all/@with-child-controls='no'"/>
<xsl:sequence select="some $c in ($importing/exclude-controls/with-id) satisfies ($c = $candidate/@id)"/>
<xsl:sequence select="some $c in ($importing/exclude-controls[o:calls-parents(.)]/with-id)
satisfies ($c = $candidate/descendant::control/@id)"/>
<xsl:sequence select="some $c in ($importing/exclude-controls[o:calls-children(.)]/with-id)
satisfies ($c = $candidate/ancestor::control/@id)"/>
<xsl:sequence select="some $m in ($importing/exclude-controls/matching[@pattern != ''])
satisfies (matches($candidate/@id,$m/@pattern/o:glob-as-regex(string(.))))"/>
<xsl:sequence select="some $m in ($importing/exclude-controls[o:calls-parents(.)]/matching[@pattern != '']), $a in $candidate/descendant::control
satisfies (matches($a/@id,$m/@pattern/o:glob-as-regex(string(.))))"/>
<xsl:sequence select="some $m in ($importing/exclude-controls[o:calls-children(.)]/matching[@pattern != '']), $a in $candidate/ancestor::control
satisfies (matches($a/@id,$m/@pattern/o:glob-as-regex(string(.))))"/>
</xsl:variable>
<!-- predicate [.] filters reasons as booleans -->
<xsl:sequence select="exists($include-reasons[.]) and empty($exclude-reasons[.])"/>
</xsl:function>

<xsl:function name="o:calls-children" as="xs:boolean">
<xsl:param name="caller" as="element()"/>
<xsl:sequence select="$caller/@with-child-controls='yes'"/>
</xsl:function>

<xsl:function name="o:calls-parents" as="xs:boolean">
<xsl:param name="caller" as="element()"/>
<xsl:sequence select="not($caller/@with-parent-controls='no')"/>
</xsl:function>

<!-- Returns a document when found, a fatal error when not. -->
<xsl:function name="o:resource-or-error" as="document-node()">
<xsl:param name="href" as="attribute(href)"/>
Expand All @@ -232,7 +156,7 @@
<xsl:sequence select="document($resolved-href)"/>
</xsl:function>

<xsl:include href="oscal-profile-resolve-functions.xsl"/>
<xsl:include href="select-or-custom-merge.xsl"/>

<xsl:function name="o:resolve-profile">
<xsl:param name="profile" as="element(profile)"/>
Expand Down
Loading

0 comments on commit 6587e35

Please sign in to comment.