-
Notifications
You must be signed in to change notification settings - Fork 8
Convert Implemented Requirements Counts to Dynamic Value Sets #26
Changes from all commits
c03e0b7
56c8b2a
f74883a
e71e0fd
33c97d3
e875549
c367072
17d9f4a
b5ec63f
6733a18
cd2bf3c
70e6d14
f3b6668
921cb91
07108bd
ab5bf7d
3f53b44
192a436
82bc5ec
53d6d3c
49cadad
04c5bca
1ec2799
e5e1b05
c2ac489
6fe6ba8
5e5e2b2
572cfc5
734f550
60bc0a0
83b7454
136a390
d574b7c
f055c0a
5f7f0d4
71be77a
f6fc823
8c69ca3
716e66c
0c60d80
e2f1aae
416348b
76f60e6
7145eba
d85a6ca
a77ae40
00a8138
cdec1ba
b54945f
43bf265
2a7f8c5
fd381e6
a1ddbf3
8b19ae4
0554bdd
d9571e8
5f63a33
a7227d9
68542e2
1b1d1ff
34be6b1
1165a02
2a26f03
022faf4
4bf7445
54623b2
5f4eada
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,86 +1,235 @@ | ||
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron" queryBinding="xslt2" | ||
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" | ||
xmlns:o="http://csrc.nist.gov/ns/oscal/1.0"> | ||
xmlns:o="http://csrc.nist.gov/ns/oscal/1.0" | ||
xpath-default-namespace="http://csrc.nist.gov/ns/oscal/1.0"> | ||
|
||
<sch:ns prefix="f" uri="https://fedramp.gov/ns/oscal"/> | ||
<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"/> | ||
<sch:ns prefix="lv" uri="local-validations"/> | ||
|
||
<sch:title>FedRAMP System Security Plan Validations</sch:title> | ||
|
||
<!-- | ||
Use XSL collection to load FedRAMP values, information types, and threats | ||
from a known source in a relative path instead of hard-coding filenames. | ||
All files are XML, but for future-proofing we filter to retrieve only XML | ||
files. | ||
--> | ||
|
||
<!-- | ||
This workaround is only to allow XSpec to source the proper context for | ||
XPath at the global level. We use XSpec for unit testing, and this is a | ||
known issue with very well-documented work-arounds. | ||
|
||
https://gitter.im/usnistgov-OSCAL/FedRAMP-10x-Schematron?at=5fa06e38f2fd4f60fc4ccec7 | ||
|
||
https://github.com/xspec/xspec/issues/873 | ||
https://github.com/xspec/xspec/issues/892 | ||
https://github.com/xspec/xspec/issues/1239 | ||
|
||
See the updated documentation below about the global-context-item pattern. | ||
|
||
https://github.com/xspec/xspec/wiki/Writing-Scenarios/ec19017ab00d769b49786cb227e57eaa2e4ee2b2#global-context-item | ||
https://github.com/AirQuick/xspec/tree/14ccd455a0e420c97903c06f0faea86719031044/tutorial/global-context-item | ||
|
||
If not, you will definitely see this error like below when running the test suite. | ||
|
||
XPDY0002 Finding root of root/key-name the context item is absent | ||
--> | ||
<xsl:param as="document-node(element(o:system-security-plan))" name="global-context-item" select="." /> | ||
<xsl:param name="fedramp-registry-href" select="'../../xml?select=*.xml'" /> | ||
<xsl:variable name="fedramp-registry" select="collection($fedramp-registry-href)"/> | ||
<xsl:variable name="selected-sensitivty-level" select="$global-context-item/o:system-security-plan/o:system-characteristics/o:security-sensitivity-level"/> | ||
|
||
<sch:let name="sensitivity-levels" value="$fedramp-registry/f:fedramp-values/f:value-set[@name='security-sensitivity-level']/f:allowed-values/f:enum/@value"/> | ||
<sch:let name="implementation-statuses" value="$fedramp-registry/f:fedramp-values/f:value-set[@name='control-implementation-status']/f:allowed-values/f:enum/@value"/> | ||
|
||
<xsl:variable name="profile-map"> | ||
<profile level="low" href="../../../baselines/xml/FedRAMP_LOW-baseline-resolved-profile_catalog.xml"/> | ||
<profile level="moderate" href="../../../baselines/xml/FedRAMP_MODERATE-baseline-resolved-profile_catalog.xml"/> | ||
<profile level="high" href="../../../baselines/xml/FedRAMP_HIGH-baseline-resolved-profile_catalog.xml"/> | ||
</xsl:variable> | ||
|
||
<xsl:key name="profile-lookup" match="profile" use="@level"/> | ||
<xsl:variable name="selected-profile-href" select="key('profile-lookup', $selected-sensitivty-level, $profile-map)/@href"/> | ||
<xsl:variable name="selected-profile" select="doc(resolve-uri($selected-profile-href))"/> | ||
<xsl:output method="xml" indent="yes" encoding="UTF-8"/> | ||
|
||
<sch:pattern> | ||
<sch:rule context="/"> | ||
<sch:assert role="error" id="no-fedramp-registry-values" test="exists($fedramp-registry/f:fedramp-values)">The FedRAMP Registry values are not present, this configuration is invalid.</sch:assert> | ||
</sch:rule> | ||
</sch:pattern> | ||
<xsl:param name="registry-href" select="'../../xml?select=*.xml'"/> | ||
|
||
<xsl:function name="lv:if-empty-default"> | ||
<xsl:param name="item"/> | ||
<xsl:param name="default"/> | ||
<xsl:choose> | ||
<!-- Atomic types, integers, strings, et cetera. --> | ||
<xsl:when test="$item instance of xs:untypedAtomic or | ||
$item instance of xs:anyURI or | ||
$item instance of xs:string or | ||
$item instance of xs:QName or | ||
$item instance of xs:boolean or | ||
$item instance of xs:base64Binary or | ||
$item instance of xs:hexBinary or | ||
$item instance of xs:integer or | ||
$item instance of xs:decimal or | ||
$item instance of xs:float or | ||
$item instance of xs:double or | ||
$item instance of xs:date or | ||
$item instance of xs:time or | ||
$item instance of xs:dateTime or | ||
$item instance of xs:dayTimeDuration or | ||
$item instance of xs:yearMonthDuration or | ||
$item instance of xs:duration or | ||
$item instance of xs:gMonth or | ||
$item instance of xs:gYear or | ||
$item instance of xs:gYearMonth or | ||
$item instance of xs:gDay or | ||
$item instance of xs:gMonthDay"> | ||
<xsl:value-of select="if ($item => string() => normalize-space() = '') then $default else $item"/> | ||
</xsl:when> | ||
<!-- Any node-kind that can be a sequence type --> | ||
<xsl:when test="$item instance of element() or | ||
$item instance of attribute() or | ||
$item instance of text() or | ||
$item instance of node() or | ||
$item instance of document-node() or | ||
$item instance of comment() or | ||
$item instance of processing-instruction()"> | ||
<xsl:sequence select="if ($item => normalize-space() => not()) then $default else $item"/> | ||
</xsl:when> | ||
<xsl:otherwise> | ||
<!-- | ||
If no suitable type found, return empty sequence, as that can | ||
be falsey and cast to empty string or checked for `not(exist(.))` | ||
later. | ||
--> | ||
<xsl:sequence select="()"/> | ||
</xsl:otherwise> | ||
</xsl:choose> | ||
</xsl:function> | ||
|
||
<xsl:function name="lv:registry" as="item()*"> | ||
<xsl:param name="href"/> | ||
<xsl:variable name="collection" select="$href => collection()"/> | ||
<xsl:choose> | ||
<xsl:when test="$collection => exists()"> | ||
<xsl:sequence select="$collection"/> | ||
</xsl:when> | ||
<xsl:otherwise> | ||
<xsl:sequence> | ||
<fedramp-values xmlns="https://fedramp.gov/ns/oscal"/> | ||
<fedramp-threats xmlns="https://fedramp.gov/ns/oscal"/> | ||
<information-types/> | ||
</xsl:sequence> | ||
</xsl:otherwise> | ||
</xsl:choose> | ||
</xsl:function> | ||
|
||
<xsl:function name="lv:sensitivity-level" as="xs:string"> | ||
<xsl:param name="context" as="node()*"/> | ||
<xsl:value-of select="$context//o:security-sensitivity-level"/> | ||
</xsl:function> | ||
|
||
<xsl:function name="lv:profile" as="document-node()*"> | ||
<xsl:param name="level" /> | ||
<xsl:variable name="profile-map"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could this be gathered from an array of keys (low, moderate, high) and parameterized into the URL following a convention? Is that more trouble than it's worth? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As discussed in our pairing for this after formal review, the deal-breaker here is you cannot have I hope to revisit better encapsulation of global parameters again when xspec/xspec#1294 is merged and xspec/xspec#991 is worked on as well. I am trying to work with upstream devs to know if I can contribute here. |
||
<profile level="low" href="../../../baselines/xml/FedRAMP_LOW-baseline-resolved-profile_catalog.xml"/> | ||
<profile level="moderate" href="../../../baselines/xml/FedRAMP_MODERATE-baseline-resolved-profile_catalog.xml"/> | ||
<profile level="high" href="../../../baselines/xml/FedRAMP_HIGH-baseline-resolved-profile_catalog.xml"/> | ||
</xsl:variable> | ||
<xsl:variable name="href" select="$profile-map/profile[@level=$level]/@href"/> | ||
<xsl:sequence select="doc(resolve-uri($href))"/> | ||
</xsl:function> | ||
|
||
<xsl:function name="lv:correct"> | ||
<xsl:param name="value-set" as="element()*"/> | ||
<xsl:param name="value" as="node()*"/> | ||
<xsl:variable name="values" select="$value-set/f:allowed-values/f:enum/@value"/> | ||
<xsl:choose> | ||
<!-- If allow-other is set, anything is valid. --> | ||
<xsl:when test="$value-set/f:allowed-values/@allow-other='no' and $value = $values"/> | ||
<xsl:otherwise> | ||
<xsl:value-of select="$values" separator=", "/> | ||
</xsl:otherwise> | ||
</xsl:choose> | ||
</xsl:function> | ||
|
||
<xsl:function name="lv:analyze"> | ||
<xsl:param name="value-set" as="element()*"/> | ||
<xsl:param name="element" as="element()*"/> | ||
<xsl:choose> | ||
<xsl:when test="$value-set/f:allowed-values/f:enum/@value"> | ||
<xsl:sequence> | ||
<xsl:call-template name="analysis-template"> | ||
<xsl:with-param name="value-set" select="$value-set"/> | ||
<xsl:with-param name="element" select="$element"/> | ||
</xsl:call-template> | ||
</xsl:sequence> | ||
</xsl:when> | ||
<xsl:otherwise> | ||
<xsl:message expand-text="yes">error</xsl:message> | ||
<xsl:sequence> | ||
<xsl:call-template name="analysis-template"> | ||
<xsl:with-param name="value-set" select="$value-set"/> | ||
<xsl:with-param name="element" select="$element"/> | ||
<xsl:with-param name="errors"> | ||
<error>value-set was malformed</error> | ||
</xsl:with-param> | ||
</xsl:call-template> | ||
</xsl:sequence> | ||
</xsl:otherwise> | ||
</xsl:choose> | ||
</xsl:function> | ||
|
||
<xsl:function name="lv:report" as="xs:string"> | ||
<xsl:param name="analysis" as="element()*"/> | ||
<xsl:variable name="results" as="xs:string"> | ||
<xsl:call-template name="report-template"> | ||
<xsl:with-param name="analysis" select="$analysis"/> | ||
</xsl:call-template> | ||
</xsl:variable> | ||
<xsl:value-of select="$results"/> | ||
</xsl:function> | ||
|
||
<xsl:template name="analysis-template" as="element()"> | ||
<xsl:param name="value-set" as="element()*"/> | ||
<xsl:param name="element" as="element()*"/> | ||
<xsl:param name="errors" as="node()*"/> | ||
<xsl:variable name="ok-values" select="$value-set/f:allowed-values/f:enum/@value"/> | ||
<analysis> | ||
<errors> | ||
<xsl:if test="$errors"><xsl:sequence select="$errors"/></xsl:if> | ||
</errors> | ||
<reports | ||
name="{$value-set/@name}" | ||
formal-name="{$value-set/f:formal-name}" | ||
description="{$value-set/f:description}" | ||
count="{count($element)}"> | ||
<xsl:for-each select="$ok-values"> | ||
<xsl:variable name="match" select="$element[@value=current()]"/> | ||
<report value="{current()}" count="{count($match)}"> | ||
</report> | ||
</xsl:for-each> | ||
</reports> | ||
</analysis> | ||
</xsl:template> | ||
|
||
<xsl:template name="report-template" as="xs:string"> | ||
<xsl:param name="analysis" as="element()*"/> | ||
<xsl:value-of> | ||
There are <xsl:value-of select="$analysis/reports/@count"/> <xsl:value-of select="$analysis/reports/@formal-name"/> | ||
<xsl:choose> | ||
<xsl:when test="$analysis/reports/report"> items total, with </xsl:when> | ||
<xsl:otherwise> items total. </xsl:otherwise> | ||
</xsl:choose> | ||
<xsl:for-each select="$analysis/reports/report"> | ||
<xsl:if test="position() gt 1 and not(position() eq last())"> | ||
<xsl:value-of select="current()/@count"/> set as <xsl:value-of select="current()/@value"/>, </xsl:if> | ||
<xsl:if test="position() gt 1 and position() eq last()" | ||
> and <xsl:value-of select="current()/@count"/> set as <xsl:value-of select="current()/@value"/>.</xsl:if> | ||
<xsl:sequence select="."/> | ||
</xsl:for-each> | ||
There are <xsl:value-of select="($analysis/reports/@count - sum($analysis/reports/report/@count))"/> invalid items. | ||
<xsl:if test="count($analysis/errors/error) > 0"> | ||
<xsl:message expand-text="yes">hit error block</xsl:message> | ||
<xsl:for-each select="$analysis/errors/error"> | ||
Also, <xsl:value-of select="current()/text()"/>, so analysis could be inaccurate or it completely failed. | ||
</xsl:for-each> | ||
</xsl:if> | ||
</xsl:value-of> | ||
</xsl:template> | ||
|
||
<sch:pattern> | ||
<sch:rule context="/o:system-security-plan/o:system-characteristics/o:security-sensitivity-level"> | ||
<sch:assert id="no-security-sensitivity-level" test="$selected-sensitivty-level">No sensitivty level found from XPath query.</sch:assert> | ||
<sch:assert id="invalid-security-sensitivity-level" test=". = $sensitivity-levels"><sch:value-of select="./name()"/> is an invalid value <sch:value-of select="."/>, not from <sch:value-of select="$sensitivity-levels" /></sch:assert> | ||
</sch:rule> | ||
</sch:pattern> | ||
<sch:pattern> | ||
<sch:rule context="/o:system-security-plan"> | ||
<sch:let name="all" value="o:control-implementation/o:implemented-requirement[o:annotation[@name='implementation-status']]"/> | ||
<sch:let name="planned" value="o:control-implementation/o:implemented-requirement[o:annotation[@name='implementation-status' and @value='planned']]"/> | ||
<sch:let name="partial" value="o:control-implementation/o:implemented-requirement[o:annotation[@name='implementation-status' and @value='partial']]"/> | ||
<sch:assert id="invalid-implemented-requirements-count" test="count($all) > 0">There are no control implementations with statuses set.</sch:assert> | ||
<sch:report id="partial-requirements-report" test="true()">There are <sch:value-of select="count($partial)"/> partial<sch:value-of select="if (count($partial)=1) then ' control implementation' else ' control implementations'"/>.</sch:report> | ||
<sch:report id="planned-requirements-report" test="true()">There are <sch:value-of select="count($planned)"/> planned<sch:value-of select="if (count($planned)=1) then ' control implementation' else ' control implementations'"/>.</sch:report> | ||
<sch:report id="all-requirements-report" test="true()">There are <sch:value-of select="count($all)"/> total<sch:value-of select="if (count($all)=1) then ' control implementation' else ' control implementations'"/>.</sch:report> | ||
<sch:let name="registry" value="$registry-href => lv:registry()"/> | ||
<sch:let name="ok-values" value="$registry/f:fedramp-values/f:value-set[@name='security-sensitivity-level']"/> | ||
<sch:let name="sensitivity-level" value="/ => lv:sensitivity-level() => lv:if-empty-default('')"/> | ||
<sch:let name="corrections" value="lv:correct($ok-values, $sensitivity-level)"/> | ||
<sch:assert role="fatal" id="no-registry-values" test="count($registry/f:fedramp-values/f:value-set) > 0" | ||
>The registry values at the path '<sch:value-of select="$registry-href"/>' are not present, this configuration is invalid.</sch:assert> | ||
<sch:assert role="fatal" id="no-security-sensitivity-level" test="$sensitivity-level != ''">No sensitivty level found, no more validation processing can occur.</sch:assert> | ||
<sch:assert role="fatal" id="invalid-security-sensitivity-level" test="empty($ok-values) or not(exists($corrections))" | ||
><sch:value-of select="./name()"/> is an invalid value of '<sch:value-of select="lv:sensitivity-level(/)"/>', not an allowed value of <sch:value-of select="$corrections"/>. No more validation processing can occur. | ||
</sch:assert> | ||
</sch:rule> | ||
|
||
<sch:rule context="/o:system-security-plan/o:control-implementation"> | ||
<sch:let name="required" value="$selected-profile/*//o:control"/> | ||
<sch:let name="registry" value="$registry-href => lv:registry()"/> | ||
<sch:let name="sensitivity-level" value="/ => lv:sensitivity-level()"/> | ||
<sch:let name="ok-values" value="$registry/f:fedramp-values/f:value-set[@name='control-implementation-status']"/> | ||
<sch:let name="selected-profile" value="$sensitivity-level => lv:profile()"/> | ||
<sch:let name="required-controls" value="$selected-profile/*//o:control"/> | ||
<sch:let name="implemented" value="o:implemented-requirement"/> | ||
<sch:let name="missing" value="$required[not(@id = $implemented/@control-id)]"/> | ||
<sch:report id="each-required-control-report" test="true()">The following <sch:value-of select="count($required)"/><sch:value-of select="if (count($required)=1) then ' control' else ' controls'"/> are required: <sch:value-of select="$required/@id"/></sch:report> | ||
<sch:assert id="incomplete-implementation-requirements" test="true()">This SSP has not implemented <sch:value-of select="count($missing)"/><sch:value-of select="if (count($missing)=1) then ' control' else ' controls'"/>: <sch:value-of select="$missing/@id"/></sch:assert> | ||
<sch:let name="missing" value="$required-controls[not(@id = $implemented/@control-id)]"/> | ||
<sch:report id="each-required-control-report" test="count($required-controls) > 0">The following <sch:value-of select="count($required-controls)"/><sch:value-of select="if (count($required-controls)=1) then ' control' else ' controls'"/> are required: <sch:value-of select="$required-controls/@id"/></sch:report> | ||
<sch:assert id="incomplete-implementation-requirements" test="not(exists($missing))">This SSP has not implemented <sch:value-of select="count($missing)"/><sch:value-of select="if (count($missing)=1) then ' control' else ' controls'"/>: <sch:value-of select="$missing/@id"/></sch:assert> | ||
<sch:let name="results" value="$ok-values => lv:analyze(//o:implemented-requirement/o:annotation[@name='implementation-status'])"/> | ||
<sch:let name="total" value="$results/reports/@count"/> | ||
<sch:report id="control-implemented-requirements-stats" test="count($results/errors/error) = 0"><sch:value-of select="$results => lv:report() => normalize-space()"/></sch:report> | ||
</sch:rule> | ||
|
||
<sch:rule context="/o:system-security-plan/o:control-implementation/o:implemented-requirement"> | ||
<sch:let name="registry" value="$registry-href => lv:registry()"/> | ||
<sch:let name="status" value="./o:annotation[@name='implementation-status']/@value"/> | ||
<sch:let name="corrections" value="lv:correct($registry/f:fedramp-values/f:value-set[@name='control-implementation-status'], $status)"/> | ||
<sch:assert id="invalid-implementation-status" test="not(exists($corrections))">Invalid status '<sch:value-of select="$status"/>' for <sch:value-of select="./@control-id"/>, must be <sch:value-of select="$corrections"/></sch:assert> | ||
</sch:rule> | ||
|
||
</sch:pattern> | ||
</sch:schema> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this permanent? I'm fine if it is, just calling attention
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kept this in here, I think after reading the core Schematron docs this Saxon flag controls whether we will see diagnostics output at build-time or runtime when we do the processing, as source explains here and an example use here.
We are not making use of it now, but I think it will be useful for the future, so I add it in.