openscap_parser 0.1.0 → 1.0.2
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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Dockerfile +15 -0
- data/README.md +32 -10
- data/Rakefile +2 -0
- data/lib/openscap_parser.rb +14 -35
- data/lib/openscap_parser/benchmark.rb +38 -0
- data/lib/openscap_parser/benchmarks.rb +21 -0
- data/lib/openscap_parser/datastream_file.rb +15 -0
- data/lib/openscap_parser/fix.rb +55 -0
- data/lib/openscap_parser/fixes.rb +21 -0
- data/lib/openscap_parser/oval_report.rb +24 -0
- data/lib/openscap_parser/profile.rb +31 -0
- data/lib/openscap_parser/profiles.rb +7 -12
- data/lib/openscap_parser/rule.rb +51 -10
- data/lib/openscap_parser/rule_identifier.rb +21 -0
- data/lib/openscap_parser/rule_reference.rb +14 -0
- data/lib/openscap_parser/rule_references.rb +32 -0
- data/lib/openscap_parser/rule_result.rb +31 -2
- data/lib/openscap_parser/rule_results.rb +21 -0
- data/lib/openscap_parser/rules.rb +9 -8
- data/lib/openscap_parser/selectors.rb +9 -0
- data/lib/openscap_parser/set_value.rb +18 -0
- data/lib/openscap_parser/set_values.rb +21 -0
- data/lib/openscap_parser/sub.rb +18 -0
- data/lib/openscap_parser/subs.rb +38 -0
- data/lib/openscap_parser/tailoring.rb +27 -0
- data/lib/openscap_parser/tailoring_file.rb +15 -0
- data/lib/openscap_parser/tailorings.rb +22 -0
- data/lib/openscap_parser/test_result.rb +62 -0
- data/lib/openscap_parser/test_result_file.rb +12 -0
- data/lib/openscap_parser/test_results.rb +19 -0
- data/lib/openscap_parser/util.rb +10 -0
- data/lib/openscap_parser/version.rb +1 -1
- data/lib/openscap_parser/xml_file.rb +13 -0
- data/lib/openscap_parser/xml_node.rb +36 -0
- data/lib/oval/definition.rb +47 -0
- data/lib/oval/definition_result.rb +17 -0
- data/lib/oval/reference.rb +21 -0
- data/lib/railtie.rb +15 -0
- data/lib/ssg.rb +5 -0
- data/lib/ssg/downloader.rb +94 -0
- data/lib/ssg/unarchiver.rb +34 -0
- data/lib/tasks/ssg.rake +33 -0
- data/openscap_parser.gemspec +13 -10
- metadata +94 -14
- data/lib/openscap_parser/xml_report.rb +0 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 65cd8f383c3d40c907ecab37235ac6c50eeddced32dc9173b7b4b8e7526c869a
|
4
|
+
data.tar.gz: a1150af6f99b020a31f174b83a1b405b1375dd0e37300ced57daa633988a8be1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 696960d718a5eefbd435af227822ce48144c07be20df1a85ec2a93c1328bba7d8f92e823b57feafe38f479a3c3b0a3f26d365595cf247cf13de3f2160b11ccda
|
7
|
+
data.tar.gz: a40766e4211f052ad1ab60c1e41dbb2bb70ab26ee221aeaa5eca664287a86b5cd1cf199f1fe47a3be23372b5723c11cf394c68db13203333abf3e0639dd1f841
|
data/.gitignore
CHANGED
data/Dockerfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# docker build . -t openscap_parser # build the container image
|
2
|
+
# docker run -itv $PWD:/app:z openscap_parser rake # run tests
|
3
|
+
# docker run -itv $PWD:/app:z openscap_parser pry --gem # console
|
4
|
+
|
5
|
+
FROM ruby:2.5
|
6
|
+
|
7
|
+
RUN gem update bundler
|
8
|
+
|
9
|
+
WORKDIR /app
|
10
|
+
|
11
|
+
COPY . ./
|
12
|
+
|
13
|
+
RUN bundle -j4
|
14
|
+
|
15
|
+
CMD bash
|
data/README.md
CHANGED
@@ -2,8 +2,6 @@
|
|
2
2
|
|
3
3
|
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/openscap_parser`. To experiment with that code, run `bin/console` for an interactive prompt.
|
4
4
|
|
5
|
-
TODO: Delete this and the text above, and describe your gem
|
6
|
-
|
7
5
|
## Installation
|
8
6
|
|
9
7
|
Add this line to your application's Gemfile:
|
@@ -24,16 +22,30 @@ Or install it yourself as:
|
|
24
22
|
|
25
23
|
ARF/XCCDF report goes IN - Ruby hash goes OUT
|
26
24
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
25
|
+
```rb
|
26
|
+
parser = OpenscapParser::Base.new(File.read('rhel7-xccdf_org.ssgproject.content_profile_standard.xml'))
|
27
|
+
parser.host # "rhel7-insights-client.virbr0.akofink-laptop"
|
28
|
+
parser.start_time # <DateTime: 2019-08-08T17:25:50+00:00 ((2458704j,62750s,0n),+0s,2299161j)>
|
29
|
+
parser.end_time # <DateTime: 2019-08-08T17:26:45+00:00 ((2458704j,62805s,0n),+0s,2299161j)>
|
30
|
+
parser.score # 80.833328
|
31
|
+
parser.profiles # {"xccdf_org.ssgproject.content_profile_standard"=>"Standard System Security Profile for Red Hat Enterprise Linux 7"}
|
32
|
+
parser.rules # [#<OpenscapParser::Rule:0x00005576e752db7 ... >, ...]
|
33
|
+
parser.rule_results # [#<OpenscapParser::RuleResult:0x00005576e8022f60 @id="xccdf_org.ssgproject.content_rule_package_rsh_removed", @result="notselected">, ...]
|
34
|
+
|
35
|
+
# and more!
|
36
|
+
```
|
37
|
+
|
38
|
+
### Fetching SCAP Security Guide Content
|
34
39
|
|
35
|
-
|
40
|
+
This gem includes a rake task to sync content from the [ComplianceAsCode project](https://github.com/ComplianceAsCode/content). The following examples show how to download and exract datastream files from the released versions:
|
41
|
+
|
42
|
+
```sh
|
43
|
+
rake ssg:sync DATASTREAMS=latest:fedora # fetch and extract the latest fedora datastream
|
44
|
+
rake ssg:sync DATASTREAMS=v0.1.45:fedora,v0.1.45:firefox # fetch and extract tag v0.1.45 for fedora and firefox datastreams
|
45
|
+
rake ssg:sync_rhel # fetch and extract the latest released versions of the RHEL 6, 7, and 8 datastreams
|
46
|
+
```
|
36
47
|
|
48
|
+
An SSG version will be downloaded only once, even if it is specified multiple times for multiple datastreams.
|
37
49
|
|
38
50
|
## Development
|
39
51
|
|
@@ -41,6 +53,16 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
41
53
|
|
42
54
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
43
55
|
|
56
|
+
### With Docker
|
57
|
+
|
58
|
+
A Dockerfile is provided to allow a containerized development environment:
|
59
|
+
|
60
|
+
```
|
61
|
+
docker build . -t openscap_parser # build the container image
|
62
|
+
docker run -itv $PWD:/app:z openscap_parser rake # run tests
|
63
|
+
docker run -itv $PWD:/app:z openscap_parser pry --gem # console
|
64
|
+
```
|
65
|
+
|
44
66
|
## Contributing
|
45
67
|
|
46
68
|
Bug reports and pull requests are welcome on GitHub at https://github.com/elobato/openscap_parser. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
data/Rakefile
CHANGED
data/lib/openscap_parser.rb
CHANGED
@@ -1,44 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openscap_parser/version'
|
4
|
+
require 'openscap_parser/util'
|
5
|
+
require 'openscap_parser/benchmarks'
|
6
|
+
require 'openscap_parser/test_results'
|
2
7
|
require 'openscap_parser/profiles'
|
3
|
-
require 'openscap_parser/rule'
|
4
|
-
require 'openscap_parser/rule_result'
|
5
8
|
require 'openscap_parser/rules'
|
6
|
-
require 'openscap_parser/
|
7
|
-
require 'openscap_parser/
|
9
|
+
require 'openscap_parser/rule_results'
|
10
|
+
require 'openscap_parser/tailorings'
|
11
|
+
|
12
|
+
require 'openscap_parser/xml_file'
|
13
|
+
require 'openscap_parser/datastream_file'
|
14
|
+
require 'openscap_parser/test_result_file'
|
15
|
+
require 'openscap_parser/tailoring_file'
|
16
|
+
require 'openscap_parser/oval_report'
|
8
17
|
|
9
18
|
require 'date'
|
19
|
+
require 'railtie' if defined?(Rails)
|
10
20
|
|
11
21
|
module OpenscapParser
|
12
22
|
class Error < StandardError; end
|
13
|
-
|
14
|
-
class Base
|
15
|
-
include OpenscapParser::XMLReport
|
16
|
-
include OpenscapParser::Profiles
|
17
|
-
include OpenscapParser::Rules
|
18
|
-
|
19
|
-
def initialize(report)
|
20
|
-
report_xml(report)
|
21
|
-
end
|
22
|
-
|
23
|
-
def score
|
24
|
-
test_result_node.search('score').text.to_f
|
25
|
-
end
|
26
|
-
|
27
|
-
def start_time
|
28
|
-
@start_time ||= DateTime.parse(test_result_node['start-time'])
|
29
|
-
end
|
30
|
-
|
31
|
-
def end_time
|
32
|
-
@end_time ||= DateTime.parse(test_result_node['end-time'])
|
33
|
-
end
|
34
|
-
|
35
|
-
def rule_results
|
36
|
-
@rule_results ||= test_result_node.search('rule-result').map do |rr|
|
37
|
-
rule_result_oscap = RuleResult.new
|
38
|
-
rule_result_oscap.id = rr.attributes['idref'].value
|
39
|
-
rule_result_oscap.result = rr.search('result').first.text
|
40
|
-
rule_result_oscap
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
23
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openscap_parser/util'
|
4
|
+
require 'openscap_parser/xml_file'
|
5
|
+
require 'openscap_parser/rules'
|
6
|
+
require 'openscap_parser/profiles'
|
7
|
+
require 'openscap_parser/rule_references'
|
8
|
+
|
9
|
+
# Mimics openscap-ruby Benchmark interface
|
10
|
+
module OpenscapParser
|
11
|
+
class Benchmark < XmlNode
|
12
|
+
include OpenscapParser::Util
|
13
|
+
include OpenscapParser::Rules
|
14
|
+
include OpenscapParser::RuleReferences
|
15
|
+
include OpenscapParser::Profiles
|
16
|
+
|
17
|
+
def id
|
18
|
+
@id ||= @parsed_xml['id']
|
19
|
+
end
|
20
|
+
|
21
|
+
def title
|
22
|
+
@title ||= @parsed_xml.xpath('title') &&
|
23
|
+
@parsed_xml.xpath('title').text
|
24
|
+
end
|
25
|
+
|
26
|
+
def description
|
27
|
+
@description ||= newline_to_whitespace(
|
28
|
+
@parsed_xml.xpath('description') &&
|
29
|
+
@parsed_xml.xpath('description').text || ''
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
def version
|
34
|
+
@version ||= @parsed_xml.xpath('version') &&
|
35
|
+
@parsed_xml.xpath('version').text
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openscap_parser/benchmark'
|
4
|
+
|
5
|
+
module OpenscapParser
|
6
|
+
# Methods related to saving profiles and finding which hosts
|
7
|
+
# they belong to
|
8
|
+
module Benchmarks
|
9
|
+
def self.included(base)
|
10
|
+
base.class_eval do
|
11
|
+
def benchmark
|
12
|
+
@benchmark ||= OpenscapParser::Benchmark.new(parsed_xml: benchmark_node)
|
13
|
+
end
|
14
|
+
|
15
|
+
def benchmark_node(xpath = ".//Benchmark")
|
16
|
+
xpath_node(xpath)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'openscap_parser/xml_file'
|
3
|
+
require 'openscap_parser/benchmarks'
|
4
|
+
|
5
|
+
module OpenscapParser
|
6
|
+
# A class to represent a datastream (-ds.xml) XmlFile
|
7
|
+
class DatastreamFile < XmlFile
|
8
|
+
include OpenscapParser::Benchmarks
|
9
|
+
|
10
|
+
def valid?
|
11
|
+
return true if @parsed_xml.root.name == 'data-stream-collection' && namespaces.keys.include?('xmlns:ds')
|
12
|
+
false
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'openscap_parser/xml_node'
|
3
|
+
require 'openscap_parser/subs'
|
4
|
+
|
5
|
+
module OpenscapParser
|
6
|
+
class Fix < XmlNode
|
7
|
+
include OpenscapParser::Subs
|
8
|
+
|
9
|
+
def id
|
10
|
+
@id ||= @parsed_xml['id']
|
11
|
+
end
|
12
|
+
|
13
|
+
def system
|
14
|
+
@system ||= @parsed_xml['system']
|
15
|
+
end
|
16
|
+
|
17
|
+
def complexity
|
18
|
+
@complexity ||= @parsed_xml['complexity']
|
19
|
+
end
|
20
|
+
|
21
|
+
def disruption
|
22
|
+
@disruption ||= @parsed_xml['disruption']
|
23
|
+
end
|
24
|
+
|
25
|
+
def strategy
|
26
|
+
@strategy ||= @parsed_xml['strategy']
|
27
|
+
end
|
28
|
+
|
29
|
+
def full_text(set_values)
|
30
|
+
full_text_lines(set_values).join('')
|
31
|
+
end
|
32
|
+
|
33
|
+
def full_text_lines(set_values)
|
34
|
+
map_child_nodes(set_values).map do |text_node|
|
35
|
+
text_node.respond_to?(:text) ? text_node.text : ''
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def map_child_nodes(set_values = [])
|
40
|
+
map_sub_nodes @parsed_xml.children, set_values
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_h
|
44
|
+
{
|
45
|
+
:id => id,
|
46
|
+
:system => system,
|
47
|
+
:complexity => complexity,
|
48
|
+
:disruption => disruption,
|
49
|
+
:strategy => strategy,
|
50
|
+
:text => text,
|
51
|
+
:subs => subs.map(&:to_h)
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openscap_parser/fix'
|
4
|
+
|
5
|
+
module OpenscapParser
|
6
|
+
module Fixes
|
7
|
+
def self.included(base)
|
8
|
+
base.class_eval do
|
9
|
+
def fixes
|
10
|
+
@fixes ||= fix_nodes.map do |fix_node|
|
11
|
+
OpenscapParser::Fix.new(parsed_xml: fix_node)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def fix_nodes(xpath = ".//fix")
|
16
|
+
xpath_nodes(xpath)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'openscap_parser/xml_file'
|
3
|
+
require 'oval/definition_result'
|
4
|
+
require 'oval/definition'
|
5
|
+
|
6
|
+
module OpenscapParser
|
7
|
+
class OvalReport < XmlFile
|
8
|
+
def definition_results
|
9
|
+
@definition_results ||= definition_result_nodes.map { |node| ::Oval::DefinitionResult.new parsed_xml: node }
|
10
|
+
end
|
11
|
+
|
12
|
+
def definition_result_nodes(xpath = "./oval_results/results/system/definitions/definition")
|
13
|
+
xpath_nodes(xpath)
|
14
|
+
end
|
15
|
+
|
16
|
+
def definitions
|
17
|
+
@definitions ||= definition_nodes.map { |node| Oval::Definition.new parsed_xml: node }
|
18
|
+
end
|
19
|
+
|
20
|
+
def definition_nodes(xpath = "./oval_results/oval_definitions/definitions/definition")
|
21
|
+
xpath_nodes(xpath)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module OpenscapParser
|
2
|
+
class Profile < XmlNode
|
3
|
+
def id
|
4
|
+
@id ||= @parsed_xml['id']
|
5
|
+
end
|
6
|
+
|
7
|
+
def extends_profile_id
|
8
|
+
@extends ||= @parsed_xml['extends']
|
9
|
+
end
|
10
|
+
|
11
|
+
def title
|
12
|
+
@title ||= @parsed_xml.at_css('title') &&
|
13
|
+
@parsed_xml.at_css('title').text
|
14
|
+
end
|
15
|
+
alias :name :title
|
16
|
+
|
17
|
+
def description
|
18
|
+
@description ||= @parsed_xml.at_css('description') &&
|
19
|
+
@parsed_xml.at_css('description').text
|
20
|
+
end
|
21
|
+
|
22
|
+
def selected_rule_ids
|
23
|
+
@selected_rule_ids ||= @parsed_xml.xpath("select[@selected='true']/@idref") &&
|
24
|
+
@parsed_xml.xpath("select[@selected='true']/@idref").map(&:text)
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_h
|
28
|
+
{ :id => id, :title => title, :description => description }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'openscap_parser/profile'
|
4
|
+
|
3
5
|
module OpenscapParser
|
4
6
|
# Methods related to saving profiles and finding which hosts
|
5
7
|
# they belong to
|
@@ -7,20 +9,13 @@ module OpenscapParser
|
|
7
9
|
def self.included(base)
|
8
10
|
base.class_eval do
|
9
11
|
def profiles
|
10
|
-
@profiles ||=
|
11
|
-
|
12
|
-
|
13
|
-
end
|
14
|
-
|
15
|
-
private
|
16
|
-
|
17
|
-
def profile_node
|
18
|
-
@report_xml.at_xpath(".//xmlns:Profile\
|
19
|
-
[contains('#{test_result_node['id']}', @id)]")
|
12
|
+
@profiles ||= profile_nodes.map do |profile_node|
|
13
|
+
OpenscapParser::Profile.new(parsed_xml: profile_node)
|
14
|
+
end
|
20
15
|
end
|
21
16
|
|
22
|
-
def
|
23
|
-
|
17
|
+
def profile_nodes(xpath = ".//Profile")
|
18
|
+
xpath_nodes(xpath)
|
24
19
|
end
|
25
20
|
end
|
26
21
|
end
|
data/lib/openscap_parser/rule.rb
CHANGED
@@ -1,31 +1,72 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'openscap_parser/rule_identifier'
|
4
|
+
require 'openscap_parser/rule_references'
|
5
|
+
require 'openscap_parser/fixes'
|
6
|
+
require 'openscap_parser/xml_file'
|
7
|
+
|
3
8
|
# Mimics openscap-ruby Rule interface
|
4
9
|
module OpenscapParser
|
5
|
-
class Rule
|
6
|
-
|
7
|
-
|
8
|
-
|
10
|
+
class Rule < XmlNode
|
11
|
+
include OpenscapParser::Util
|
12
|
+
include OpenscapParser::RuleReferences
|
13
|
+
include OpenscapParser::Fixes
|
9
14
|
|
10
15
|
def id
|
11
|
-
@id ||=
|
16
|
+
@id ||= parsed_xml['id']
|
17
|
+
end
|
18
|
+
|
19
|
+
def selected
|
20
|
+
@selected ||= parsed_xml['selected']
|
12
21
|
end
|
13
22
|
|
14
23
|
def severity
|
15
|
-
@severity ||=
|
24
|
+
@severity ||= parsed_xml['severity']
|
16
25
|
end
|
17
26
|
|
18
27
|
def title
|
19
|
-
@title ||=
|
28
|
+
@title ||= parsed_xml.at_css('title') &&
|
29
|
+
parsed_xml.at_css('title').text
|
20
30
|
end
|
21
31
|
|
22
32
|
def description
|
23
|
-
@description ||=
|
33
|
+
@description ||= newline_to_whitespace(
|
34
|
+
parsed_xml.at_css('description') &&
|
35
|
+
parsed_xml.at_css('description').text || ''
|
36
|
+
)
|
24
37
|
end
|
25
38
|
|
26
39
|
def rationale
|
27
|
-
@rationale ||=
|
40
|
+
@rationale ||= newline_to_whitespace(
|
41
|
+
parsed_xml.at_css('rationale') &&
|
42
|
+
parsed_xml.at_css('rationale').text || ''
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
alias :rule_reference_nodes_old :rule_reference_nodes
|
47
|
+
def rule_reference_nodes(xpath = "reference")
|
48
|
+
rule_reference_nodes_old(xpath)
|
49
|
+
end
|
50
|
+
|
51
|
+
def rule_identifier
|
52
|
+
@identifier ||= RuleIdentifier.new(parsed_xml: identifier_node)
|
53
|
+
end
|
54
|
+
alias :identifier :rule_identifier
|
55
|
+
|
56
|
+
def identifier_node
|
57
|
+
@identifier_node ||= parsed_xml.at_xpath('ident')
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_h
|
61
|
+
{
|
62
|
+
:id => id,
|
63
|
+
:selected => selected,
|
64
|
+
:severity => severity,
|
65
|
+
:title => title,
|
66
|
+
:description => description,
|
67
|
+
:rationale => rationale,
|
68
|
+
:identifier => rule_identifier.to_h
|
69
|
+
}
|
28
70
|
end
|
29
71
|
end
|
30
72
|
end
|
31
|
-
|