Skip to content

Commit

Permalink
Improvements to Schematron for SP800-53 usnistgov#400
Browse files Browse the repository at this point in the history
  • Loading branch information
wendellpiez committed Sep 18, 2019
1 parent 47d9624 commit 48fc8df
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,103 +7,98 @@
<sch:ns prefix="o" uri="http://csrc.nist.gov/ns/oscal/1.0"/>
<sch:ns prefix="oscal" uri="http://csrc.nist.gov/ns/oscal/1.0"/>

<xsl:key name="internal-links" match="*[@id]" use="'#' || @id"/>

<!-- This Schematron checks the representation of labels in SP800-53
- labels given on controls and their parts correspond to their @id values
- labels are formatted correctly and represent the order of the document
by incrementing regularly -->

<sch:pattern>
<sch:rule context="oscal:part[@name='item']/oscal:prop[@name='label']">
<xsl:variable name="number-format">
<xsl:apply-templates mode="number-format" select=".."/>
</xsl:variable>
<!-- returns true() when $parent-label is empty -->
<xsl:variable name="expected">
<xsl:number count="oscal:part[@name='item']" format="{$number-format}"/>
</xsl:variable>
<sch:assert test=". = $expected">Label issue: expected '<sch:value-of select="$expected"/>'</sch:assert>
</sch:rule>

<sch:rule context="oscal:prop[@class='label']">
<sch:let name="parent-label" value="parent::*/../oscal:prop[@class='label']"/>
<!-- returns true() when $parent-label is empty -->
<!-- Preempted by the preceding rule, this rule matches label properties not directly inside part[@name='item'] -->
<sch:rule context="oscal:prop[@name='label']">
<sch:let name="parent-label" value="parent::*/../oscal:prop[@name='label']"/>
<!-- returns true() when $parent-label is empty -->
<sch:assert test="starts-with(.,$parent-label)">Label hierarchy issue</sch:assert>
</sch:rule>

<sch:rule context="oscal:control">
<sch:let name="expected-id" value="o:reduce-label(oscal:prop[@class='label'])"/>
<sch:let name="expected-id" value="o:reduce-label(oscal:prop[@name='label'])"/>
<sch:assert test="@id = $expected-id">Expected id to be '<sch:value-of select="$expected-id"/>'</sch:assert>
</sch:rule>

<sch:rule context="oscal:control/oscal:part[@class='statement']">
<sch:rule context="oscal:control/oscal:part[@name='statement']">
<sch:let name="expected-id" value="../@id || '_smt'"/>
<sch:assert test="@id = $expected-id">Expected id to be '<sch:value-of select="$expected-id"/>'</sch:assert>
</sch:rule>

<sch:rule context="oscal:part[@class='statement']/oscal:part[@class='statement']">
<sch:rule context="oscal:part[@name='statement']/oscal:part[@name='statement']">
<sch:let name="owner" value="(ancestor::oscal:control)[1]"/>
<xsl:variable name="reckoning">
<xsl:number count="oscal:part[@class='statement']/oscal:part[@class='statement']" level="multiple" format=".a.1"/>
<xsl:number count="oscal:part[@name='statement']/oscal:part[@name='statement']" level="multiple" format=".a.1"/>
</xsl:variable>
<sch:let name="expected-id" value="$owner/@id || '_smt' || $reckoning"/>
<sch:assert test="@id = $expected-id">Expected id to be '<sch:value-of select="$expected-id"/>'</sch:assert>
</sch:rule>

<sch:rule context="oscal:control/oscal:part[@class='objective']">
<sch:rule context="oscal:control/oscal:part[@name='objective']">
<sch:let name="expected-id" value="../@id || '_obj'"/>
<sch:assert test="@id = $expected-id">Expected id to be '<sch:value-of select="$expected-id"/>'</sch:assert>
</sch:rule>

<!--<sch:rule context="oscal:part[@class='objective']/oscal:part[@class='objective'][ends-with(@id,'_obj')]"/>-->

<!-- The id has two parts, before and after '_obj' called $head and $tail
<!-- OBJECTIVE IDS
An 'objective' id has two parts, before and after '_obj' called $head and $tail
One but not the other of these has an incremented value.
The other must be the same as the parent.
If neither or both are the same we have an error.
calling the kind that increments the head, a 'matcher'
calling the kind that increments the tail, a 'brancher'
the head serves to match to
-->
calling the kind that increments the tail, a 'brancher' -->
<!-- when the head is incremented, it should be calculable relative to other
front-incremented values (heads, numbered relative to the root of the control)
when the tail is incremented, similarly, within the scope of elements with the same head -->

<sch:rule context="oscal:part[@class='objective']/oscal:part[@class='objective']">
<sch:rule context="oscal:part[@name = 'objective']/oscal:part[@name = 'objective']">

<sch:let name="owner" value="ancestor::oscal:control[1]"/>

<sch:let name="my-head" value="o:head(.)"/>
<sch:let name="my-tail" value="o:tail(.)"/>
<sch:assert test="(o:head(.)=o:head(..)) or (o:tail(.)=o:tail(..))">Head or tail must match</sch:assert>
<!-- $maps when the head increments, meaning we map to a new element -->
<sch:let name="maps" value="$my-tail = (o:tail(..), '')"/>
<!-- $extends when the head is the same but we extend for more detail (the tail changes) -->
<sch:assert test="(o:head(.) = o:head(..)) or (o:tail(.) = o:tail(..))">Head or tail must match</sch:assert>
<!-- $maps when the head increments, meaning we map to a new element -->
<sch:let name="maps" value="$my-tail = (o:tail(..), '')"/>
<!-- $extends when the head is the same but we extend for more detail (the tail changes) -->
<sch:let name="extends" value="o:head(.) = o:head(..)"/>
<!-- When $maps, we expect the mapping portion (the head) to be regular -->

<!-- When $maps, we expect the mapping portion (the head) to be regular -->
<xsl:variable name="id-for-mapper">
<xsl:value-of select="o:head(..)"/>
<xsl:number count="oscal:part/oscal:part" format="{
if (matches(o:head(..),'\d$')) then '.a' else '.1'
}"/>
<xsl:number count="oscal:part/oscal:part"
format="{ if (matches(o:head(..),'\d$')) then '.a' else '.1' }"/>
<xsl:text>_obj</xsl:text>
<xsl:value-of select="$my-tail"/>
</xsl:variable>

<!-- <sch:report role="info" test="$maps"><sch:value-of select="$id-for-mapper"/></sch:report>-->
<sch:assert test="not($maps) or @id=$id-for-mapper">
Expecting @id to be '<sch:value-of select="$id-for-mapper"/>'
</sch:assert>


<!--<sch:report test="$extends" role="info">Extends!<!-\- <sch:value-of select="$nomination"/>-\-></sch:report>-->

<sch:assert test="not($maps) or @id = $id-for-mapper"> Expecting @id to be
'<sch:value-of select="$id-for-mapper"/>' </sch:assert>

<xsl:variable name="id-for-extender">
<xsl:value-of select="../@id"/>
<xsl:number count="oscal:part/oscal:part" format="{
if (matches(../@id,'\d$')) then '.a' else '.1'
}"/>
<xsl:number count="oscal:part/oscal:part"
format="{ if (matches(../@id,'\d$')) then '.a' else '.1' }"/>
</xsl:variable>
<sch:assert test="not($extends) or @id=$id-for-extender">
Expecting @id to be '<sch:value-of select="$id-for-extender"/>'
</sch:assert>
</sch:rule>

<!--<sch:assert test="starts-with(@id,../@id)">ID out of place?</sch:assert>-->




<sch:rule context="oscal:link[@rel=('corresp','related')]">
<sch:let name="exception" value=".='Appendix J'"/>
<sch:assert test="exists(key('internal-links',@href)) or $exception">Broken internal link</sch:assert>
<sch:assert test="not($extends) or @id = $id-for-extender"> Expecting @id to be
'<sch:value-of select="$id-for-extender"/>' </sch:assert>
</sch:rule>

</sch:pattern>
Expand All @@ -123,4 +118,8 @@
<xsl:value-of select="lower-case($what) ! replace(., '\(', '.') ! replace(., '\)', '') ! replace(.,'\.$','')"/>
</xsl:function>

<xsl:template mode="number-format" priority="1" match="oscal:part[@name='item']">a.</xsl:template>
<xsl:template mode="number-format" priority="2" match="oscal:part[@name='item']/oscal:part[@name='item']">1.</xsl:template>
<xsl:template mode="number-format" priority="3" match="oscal:control/oscal:control//oscal:part[@name='item']">(a)</xsl:template>
<xsl:template mode="number-format" priority="4" match="oscal:control/oscal:control//oscal:part[@name='item']/oscal:part[@name='item']">(1)</xsl:template>
</sch:schema>
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,33 @@
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:o="http://csrc.nist.gov/ns/oscal/1.0">

<sch:ns prefix="o" uri="http://csrc.nist.gov/ns/oscal/1.0"/>
<sch:ns prefix="o" uri="http://csrc.nist.gov/ns/oscal/1.0"/>

<xsl:key name="by-id" match="*[@id]" use="@id"/>
<xsl:key name="link-targets" match="*[@id]" use="'#' || @id"/>

<!-- This Schematron checks several basic features of SP800-53, or a similarly encoded document,
for conformance with expectations. Specifically, it tests:
- @id values are unique in the document
- Use of @name is consistent with OSCAL, avoiding clashes with named elements in OSCAL
- Expected (named) parts and properties are given in controls and subcontrols
- Parts and properties are not given redundantly; when expected, they are singletons
(the only child of their parent node with the @name value)
- Cross-references (internal links) resolve when given
More complex checking of
-->

<!-- Some tokens should be prohibited for use as @name flags. -->
<sch:let name="interdicted" value="'control','group','part','prop','param','title'"/>

<sch:let name="known-part-names" value="('statement', 'item', 'guidance', 'objective', 'assessment', 'objects')"/>

<sch:let name="known-property-names" value="('keywords', 'label', 'sort-id', 'method', 'status')"/>

<sch:pattern id="general">
<sch:rule context="*[matches(@name,'\S')]">
<sch:report test="@name = $interdicted">@name '<sch:value-of select="@name"/>' is not permitted</sch:report>
Expand All @@ -24,13 +43,25 @@
</sch:rule>
</sch:pattern>

<!-- Ensure all the values of @name given are known. -->
<sch:pattern id="occurrences">
<sch:rule context="o:part[exists(@name)]">
<sch:assert test="@name=$known-part-names">@name on part is not recognized: we expect <sch:value-of select="o:or-sequence($known-part-names)"/></sch:assert>
</sch:rule>
<sch:rule context="o:prop[exists(@name)]">
<sch:assert test="@name=$known-property-names">@name on property is not recognized: we expect <sch:value-of select="o:or-sequence($known-property-names)"/></sch:assert>
</sch:rule>

</sch:pattern>

<!-- Controls and their parts may require properties or subparts designated for particular contents. -->
<sch:pattern id="required-items">
<sch:rule context="o:control">
<sch:let name="withdrawn" value="o:prop[@name='status'] = 'Withdrawn'"/>
<sch:assert test="o:prop/@name='label'">control must have a child 'prop' with @name='label'</sch:assert>
<sch:assert test="o:part/@name='statement' or $withdrawn">control with name='SP800-53' must have a child part with @name='statement'</sch:assert>
<sch:assert test="o:part/@name='objective' or $withdrawn">control with name='SP800-53' must have a child part with @name='objective'</sch:assert>
<sch:assert test="o:prop/@name='sort-id'">control must have a child 'prop' with @name='sort-id'</sch:assert>
<sch:assert test="o:part/@name='statement' or $withdrawn">control with name='SP800-53' must have a child 'part' with @name='statement'</sch:assert>
<sch:assert test="o:part/@name='objective' or $withdrawn">control with name='SP800-53' must have a child 'part' with @name='objective'</sch:assert>
</sch:rule>
<sch:rule context="o:part[@name='item']">
<sch:assert test="o:prop/@name='label'">part with name='item' must have a child prop with @name='label'</sch:assert>
Expand Down Expand Up @@ -78,6 +109,11 @@
</xsl:variable>
<sch:assert test="replace(.,'\D','') = $formatted-no">Control label appears to be out of sequence</sch:assert>
</sch:rule>
<sch:rule context="o:part[@name = 'statement']">
<sch:assert test="o:singleton(.)">part with name='statement'
must be a singleton: no other parts named 'statement' may appear in the same
context</sch:assert>
</sch:rule>
<sch:rule context="o:part[@name = 'item']/o:prop[@name = 'label']">
<sch:assert test="o:singleton(.)">prop with name='label'
must be a singleton: no other properties named 'label' may appear in the same
Expand All @@ -95,6 +131,11 @@
<sch:assert test="not(count($parent-label) = 1) or starts-with(., $parent-label)">Label is expected to start with
inherited label '<sch:value-of select="$parent-label"/>'</sch:assert>
</sch:rule>
<sch:rule context="o:prop[@name = 'sort-id']">
<sch:assert test="o:singleton(.)">prop with name='sort-id'
must be a singleton: no other properties named 'sort-id' may appear in the same
context</sch:assert>
</sch:rule>
<sch:rule context="o:part[@name = 'assessment']/o:prop[@name = 'method']">
<sch:assert test="o:singleton(.)">prop with name='method'
must be a singleton: no other properties named 'method' may appear in the same
Expand All @@ -109,6 +150,13 @@
<sch:assert test=". = ('Withdrawn')">prop name='status' here must have a value
'Withdrawn'</sch:assert>
</sch:rule>
<sch:rule context="o:prop[@name = 'method']">
<sch:assert test="o:singleton(.)">prop with name='method' must be a singleton: no other
properties named 'method' may appear in the same context</sch:assert>
</sch:rule>
<sch:rule context="o:prop[@name = 'keywords']">
<sch:assert test="exists(parent::o:metadata)">prop with name='keywords' is not expected outside metadata</sch:assert>
</sch:rule>
</sch:pattern>

<!-- Checking presence and form of link hrefs -->
Expand All @@ -128,5 +176,22 @@
<xsl:variable name="competitors" select="$who/parent::*/(* except $who)[@name = $who/@name]"/>
<xsl:sequence select="empty($competitors)"/>
</xsl:function>

<xsl:function name="o:or-sequence" as="xs:string?">
<xsl:param name="seq" as="item()*"/>
<xsl:value-of>
<xsl:for-each select="$seq ! ('''' || . || '''')">
<xsl:if test="position() ne 1">
<xsl:choose>
<xsl:when test="(position() eq 2 and last() eq 2)"> or </xsl:when>
<xsl:when test="position() = last()">, or </xsl:when>
<xsl:otherwise>, </xsl:otherwise>
</xsl:choose>
</xsl:if>
<xsl:value-of select="."/>
</xsl:for-each>
</xsl:value-of>
</xsl:function>


</sch:schema>

0 comments on commit 48fc8df

Please sign in to comment.