risu 1.4.9 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.ci +18 -0
- data/LICENSE +3 -3
- data/NEWS.markdown +45 -0
- data/README.markdown +10 -2
- data/Rakefile +15 -5
- data/TODO.markdown +45 -19
- data/lib/risu.rb +2 -1
- data/lib/risu/base/schema.rb +3 -4
- data/lib/risu/base/template_base.rb +11 -11
- data/lib/risu/cli/application.rb +23 -15
- data/lib/risu/exceptions.rb +1 -3
- data/lib/risu/models/host.rb +1 -1
- data/lib/risu/models/item.rb +90 -18
- data/lib/risu/models/plugin.rb +2 -2
- data/lib/risu/models/reference.rb +93 -0
- data/lib/risu/parsers/nessus/nessus_sax_listener.rb +28 -30
- data/lib/risu/renderers.rb +6 -0
- data/lib/risu/renderers/nilrenderer.rb +25 -0
- data/lib/risu/templates/assets.rb +5 -2
- data/lib/risu/templates/cover_sheet.rb +1 -1
- data/lib/risu/templates/exec_summary.rb +19 -4
- data/lib/risu/templates/executive_summary.rb +20 -5
- data/lib/risu/templates/finding_statistics.rb +1 -1
- data/lib/risu/templates/findings_host.rb +27 -8
- data/lib/risu/templates/findings_summary.rb +14 -1
- data/lib/risu/templates/findings_summary_with_pluginid.rb +1 -1
- data/lib/risu/templates/graphs.rb +5 -1
- data/lib/risu/templates/host_summary.rb +8 -4
- data/lib/risu/templates/ms_patch_summary.rb +1 -1
- data/lib/risu/templates/ms_update_summary.rb +1 -1
- data/lib/risu/templates/notable.rb +1 -2
- data/lib/risu/templates/notable_detailed.rb +8 -8
- data/lib/risu/templates/pci_compliance.rb +1 -1
- data/lib/risu/templates/stig_findings_summary.rb +135 -0
- data/lib/risu/templates/technical_findings.rb +5 -17
- data/lib/risu/templates/template.rb +1 -1
- data/risu.gemspec +1 -2
- metadata +43 -28
data/lib/risu/exceptions.rb
CHANGED
data/lib/risu/models/host.rb
CHANGED
@@ -319,7 +319,7 @@ module Risu
|
|
319
319
|
g.data("OSX", osx) unless osx == 0
|
320
320
|
g.data("FreeBSD", freebsd) unless freebsd == 0
|
321
321
|
g.data("NetBSD", netbsd) unless netbsd == 0
|
322
|
-
g.data("Cisco
|
322
|
+
g.data("Cisco IOS", cisco) unless cisco == 0
|
323
323
|
g.data("VxWorks", vxworks) unless vxworks == 0
|
324
324
|
g.data("VMware", esx) unless esx == 0
|
325
325
|
g.data("AIX", aix) unless aix == 0
|
data/lib/risu/models/item.rb
CHANGED
@@ -14,9 +14,16 @@ module Risu
|
|
14
14
|
#
|
15
15
|
# @return [ActiveRecord::Relation] with the query results
|
16
16
|
def risks
|
17
|
-
where(:severity => [0,1,2,3])
|
17
|
+
where(:severity => [0,1,2,3,4])
|
18
18
|
end
|
19
19
|
|
20
|
+
# Queries for all the high risks in the database
|
21
|
+
#
|
22
|
+
# @return [ActiveRecord::Relation] with the query results
|
23
|
+
def critical_risks
|
24
|
+
where(:severity => 4)
|
25
|
+
end
|
26
|
+
|
20
27
|
# Queries for all the high risks in the database
|
21
28
|
#
|
22
29
|
# @return [ActiveRecord::Relation] with the query results
|
@@ -45,6 +52,13 @@ module Risu
|
|
45
52
|
where(:severity => 0)
|
46
53
|
end
|
47
54
|
|
55
|
+
# Queries for all the unique Critical risks in the database
|
56
|
+
#
|
57
|
+
# @return [ActiveRecord::Relation] with the query results
|
58
|
+
def critical_risks_unique
|
59
|
+
where(:severity => 4).joins(:plugin).order("plugins.cvss_base_score").group(:plugin_id)
|
60
|
+
end
|
61
|
+
|
48
62
|
# Queries for all the unique high risks in the database
|
49
63
|
#
|
50
64
|
# @return [ActiveRecord::Relation] with the query results
|
@@ -52,6 +66,13 @@ module Risu
|
|
52
66
|
where(:severity => 3).joins(:plugin).order("plugins.cvss_base_score").group(:plugin_id)
|
53
67
|
end
|
54
68
|
|
69
|
+
# Queries for all the unique Critical findings and sorts them by count
|
70
|
+
#
|
71
|
+
# @return [ActiveRecord::Relation] with the query results
|
72
|
+
def critical_risks_unique_sorted
|
73
|
+
select("items.*").select("count(*) as count_all").where(:severity => 4).group(:plugin_id).order("count_all DESC")
|
74
|
+
end
|
75
|
+
|
55
76
|
# Queries for all the unique high findings and sorts them by count
|
56
77
|
#
|
57
78
|
# @return [ActiveRecord::Relation] with the query results
|
@@ -108,16 +129,16 @@ module Risu
|
|
108
129
|
select("items.*").select("count(*) as count_all").where("svc_name != 'unknown' and svc_name != 'general'").group(:svc_name).order("count_all DESC").limit(limit)
|
109
130
|
end
|
110
131
|
|
111
|
-
# Queries for all the
|
132
|
+
# Queries for all the Critical risks by plugin
|
112
133
|
#
|
113
134
|
# @param limit Limits the result to a specific number, default 10
|
114
135
|
#
|
115
136
|
# @return [ActiveRecord::Relation] with the query results
|
116
137
|
def risks_by_plugin(limit=10)
|
117
|
-
select("items.*").select("count(*) as count_all").joins(:plugin).where("plugin_id != 1").where(:severity =>
|
138
|
+
select("items.*").select("count(*) as count_all").joins(:plugin).where("plugin_id != 1").where(:severity => 4).group(:plugin_id).order("count_all DESC").limit(limit)
|
118
139
|
end
|
119
140
|
|
120
|
-
# Queries for all the
|
141
|
+
# Queries for all the Critical risks by host
|
121
142
|
#
|
122
143
|
# @param limit Limits the result to a specific number, default 10
|
123
144
|
#
|
@@ -125,7 +146,7 @@ module Risu
|
|
125
146
|
#
|
126
147
|
# @return [ActiveRecord::Relation] with the query results
|
127
148
|
def risks_by_host(limit=10)
|
128
|
-
select("items.*").select("count(*) as count_all").joins(:host).where("plugin_id != 1").where(:severity =>
|
149
|
+
select("items.*").select("count(*) as count_all").joins(:host).where("plugin_id != 1").where(:severity => 4).group(:host_id).order("count_all DESC").limit(limit)
|
129
150
|
end
|
130
151
|
|
131
152
|
# Queries for all the hosts with the Microsoft patch summary plugin (38153)
|
@@ -182,16 +203,19 @@ module Risu
|
|
182
203
|
:background_colors => %w(white white)
|
183
204
|
}
|
184
205
|
|
206
|
+
crit = Item.critical_risks.count
|
185
207
|
high = Item.high_risks.count
|
186
208
|
medium = Item.medium_risks.count
|
187
209
|
low = Item.low_risks.count
|
188
210
|
info = Item.info_risks.count
|
189
211
|
|
212
|
+
if crit == nil then crit = 0 end
|
190
213
|
if high == nil then high = 0 end
|
191
214
|
if medium == nil then medium = 0 end
|
192
215
|
if low == nil then low = 0 end
|
193
216
|
if info == nil then info = 0 end
|
194
217
|
|
218
|
+
g.data("Critical", crit, "purple")
|
195
219
|
g.data("High", high, "red")
|
196
220
|
g.data("Medium", medium, "orange")
|
197
221
|
g.data("Low", low, "yellow")
|
@@ -199,30 +223,65 @@ module Risu
|
|
199
223
|
|
200
224
|
StringIO.new(g.to_blob)
|
201
225
|
end
|
226
|
+
|
227
|
+
#
|
228
|
+
#
|
229
|
+
def stig_findings(categeory="I")
|
230
|
+
where('plugin_id IN (:plugins)', :plugins => Plugin.where(:stig_severity => categeory).select(:id)).order("severity DESC")
|
231
|
+
end
|
232
|
+
|
233
|
+
# Generates a Graph of all the risks by severity
|
234
|
+
#
|
235
|
+
# @return [StringIO] Object containing the generated PNG image
|
236
|
+
def stigs_severity_graph
|
237
|
+
g = Gruff::Bar.new(GRAPH_WIDTH)
|
238
|
+
g.title = "Stigs By Severity"
|
239
|
+
g.sort = false
|
240
|
+
g.theme = {
|
241
|
+
:colors => %w(purple red orange yellow blue green black grey brown pink),
|
242
|
+
:background_colors => %w(white white)
|
243
|
+
}
|
244
|
+
|
245
|
+
i = Item.stig_findings("I").count
|
246
|
+
ii = Item.stig_findings("II").count
|
247
|
+
iii = Item.stig_findings("III").count
|
248
|
+
|
249
|
+
if i == nil then i = 0 end
|
250
|
+
if ii == nil then ii = 0 end
|
251
|
+
if iii == nil then iii = 0 end
|
252
|
+
|
253
|
+
g.data("Cat I", i, "purple")
|
254
|
+
g.data("Cat II", ii, "red")
|
255
|
+
g.data("Cat III", iii, "orange")
|
256
|
+
|
257
|
+
StringIO.new(g.to_blob)
|
258
|
+
end
|
202
259
|
|
203
260
|
# @todo change Report.title to a real variable
|
204
261
|
# @todo rewite this
|
205
262
|
def risks_by_severity_graph_text
|
206
|
-
|
207
|
-
|
263
|
+
#crit = Item.crit_risks.count
|
264
|
+
#high = Item.high_risks.count
|
265
|
+
#medium = Item.medium_risks.count
|
208
266
|
|
209
|
-
if
|
210
|
-
if
|
267
|
+
#if crit == nil then crit = 0 end
|
268
|
+
#if high == nil then high = 0 end
|
269
|
+
#if medium == nil then medium = 0 end
|
211
270
|
|
212
271
|
#percentage = high
|
213
272
|
|
214
|
-
|
273
|
+
hosts_with_critical = Hash.new
|
215
274
|
|
216
|
-
Item.
|
275
|
+
Item.critical_risks.all.each do |item|
|
217
276
|
ip = Host.find_by_id(item.host_id).name
|
218
|
-
if
|
219
|
-
|
277
|
+
if hosts_with_critical[ip] == nil
|
278
|
+
hosts_with_critical[ip] = 1
|
220
279
|
end
|
221
280
|
|
222
|
-
|
281
|
+
hosts_with_critical[ip] = hosts_with_critical[ip] + 1
|
223
282
|
end
|
224
283
|
|
225
|
-
host_percent = (
|
284
|
+
host_percent = (hosts_with_critical.count.to_f / Host.all.count.to_f) * 100
|
226
285
|
|
227
286
|
adjective = case host_percent
|
228
287
|
when 0..5
|
@@ -286,7 +345,7 @@ module Risu
|
|
286
345
|
|
287
346
|
#sqlite only @todo @fix
|
288
347
|
def top_10_sorted_raw
|
289
|
-
raw = Item.joins(:plugin).where(:severity =>
|
348
|
+
raw = Item.joins(:plugin).where(:severity => 4).order("cast(plugins.cvss_base_score as real)").count(:all, :group => :plugin_id)
|
290
349
|
data = Array.new
|
291
350
|
|
292
351
|
raw.each do |vuln|
|
@@ -308,7 +367,7 @@ module Risu
|
|
308
367
|
|
309
368
|
def top_10_sorted
|
310
369
|
#raw = Item.where(:severity => 3).count(:all, :group => :plugin_id)
|
311
|
-
raw = Item.joins(:plugin).where(:severity =>
|
370
|
+
raw = Item.joins(:plugin).where(:severity => 4).order(:cvss_base_score).count(:all, :group => :plugin_id)
|
312
371
|
data = Array.new
|
313
372
|
|
314
373
|
raw.each do |vuln|
|
@@ -330,6 +389,12 @@ module Risu
|
|
330
389
|
return data
|
331
390
|
end
|
332
391
|
|
392
|
+
# Returns a prawn pdf table for the top 10 notable findings
|
393
|
+
#
|
394
|
+
# @todo change this method to return a array/table and let the template render it
|
395
|
+
# @todo rename to notable_table also
|
396
|
+
#
|
397
|
+
# @param output device to write the table to
|
333
398
|
def top_10_table(output)
|
334
399
|
headers = ["Description", "Count"]
|
335
400
|
header_widths = {0 => (output.bounds.width - 50), 1 => 50}
|
@@ -339,7 +404,14 @@ module Risu
|
|
339
404
|
output.table([headers] + data[0..9], :header => true, :column_widths => header_widths, :width => output.bounds.width) do
|
340
405
|
row(0).style(:font_style => :bold, :background_color => 'cccccc')
|
341
406
|
cells.borders = [:top, :bottom, :left, :right]
|
342
|
-
end
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
# Queries for all unique risks and sorts them by count
|
411
|
+
#
|
412
|
+
# @return [ActiveRecord::Relation] with the query results
|
413
|
+
def all_risks_unique_sorted
|
414
|
+
select("items.*").select("count(*) as count_all").group(:plugin_id).order("count_all DESC")
|
343
415
|
end
|
344
416
|
|
345
417
|
end
|
data/lib/risu/models/plugin.rb
CHANGED
@@ -59,7 +59,7 @@ module Risu
|
|
59
59
|
# @return Filename of the created graph
|
60
60
|
def top_by_count_graph(limit=10)
|
61
61
|
g = Gruff::Bar.new(GRAPH_WIDTH)
|
62
|
-
g.title = sprintf "Top %d
|
62
|
+
g.title = sprintf "Top %d Critical Findings By Plugin", Item.risks_by_plugin(limit).all.count
|
63
63
|
g.sort = false
|
64
64
|
g.theme = {
|
65
65
|
:colors => %w(red orange yellow blue green purple black grey brown pink),
|
@@ -80,7 +80,7 @@ module Risu
|
|
80
80
|
else
|
81
81
|
plugin_name = Plugin.find_by_id(plugin.plugin_id).plugin_name
|
82
82
|
end
|
83
|
-
|
83
|
+
|
84
84
|
if plugin_name =~ /^(MS\d{2}-\d{3}):/
|
85
85
|
plugin_name = $1
|
86
86
|
end
|
@@ -6,6 +6,99 @@ module Risu
|
|
6
6
|
# @author Jacob Hammack
|
7
7
|
class Reference < ActiveRecord::Base
|
8
8
|
has_many :plugins
|
9
|
+
|
10
|
+
class << self
|
11
|
+
|
12
|
+
# Queries all unique CVEs
|
13
|
+
#
|
14
|
+
def cve
|
15
|
+
where(:reference_name => "cve").select('DISTINCT value')
|
16
|
+
end
|
17
|
+
|
18
|
+
# Queries all unique CPE
|
19
|
+
#
|
20
|
+
def cpe
|
21
|
+
where(:reference_name => "cpe").select('DISTINCT value')
|
22
|
+
end
|
23
|
+
|
24
|
+
# Queries all unique BID
|
25
|
+
#
|
26
|
+
def bid
|
27
|
+
where(:reference_name => "bid").select('DISTINCT value')
|
28
|
+
end
|
29
|
+
|
30
|
+
# Queries all unique see_also
|
31
|
+
#
|
32
|
+
def see_also
|
33
|
+
where(:reference_name => "see_also").select('DISTINCT value')
|
34
|
+
end
|
35
|
+
|
36
|
+
# Queries all unique IAVA
|
37
|
+
#
|
38
|
+
def iava
|
39
|
+
where(:reference_name => "iava").select('DISTINCT value')
|
40
|
+
end
|
41
|
+
|
42
|
+
# Queries all unique MSFT
|
43
|
+
#
|
44
|
+
def msft
|
45
|
+
where(:reference_name => "msft").select('DISTINCT value')
|
46
|
+
end
|
47
|
+
|
48
|
+
# Queries all unique OSvdb
|
49
|
+
#
|
50
|
+
def osvdb
|
51
|
+
where(:reference_name => "osvdb").select('DISTINCT value')
|
52
|
+
end
|
53
|
+
|
54
|
+
# Queries all unqiue cert refs
|
55
|
+
#
|
56
|
+
def cert
|
57
|
+
where(:reference_name => "cert").select('DISTINCT value')
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
#
|
62
|
+
def edbid
|
63
|
+
where(:reference_name => "edb-id").select('DISTINCT value')
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
#
|
68
|
+
def rhsa
|
69
|
+
where(:reference_name => "rhsa").select('DISTINCT value')
|
70
|
+
end
|
71
|
+
|
72
|
+
#
|
73
|
+
#
|
74
|
+
def secunia
|
75
|
+
where(:reference_name => "secunia").select('DISTINCT value')
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
#
|
80
|
+
def suse
|
81
|
+
where(:reference_name => "suse").select('DISTINCT value')
|
82
|
+
end
|
83
|
+
|
84
|
+
#
|
85
|
+
#
|
86
|
+
def dsa
|
87
|
+
where(:reference_name => "dsa").select('DISTINCT value')
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
#
|
92
|
+
def owasp
|
93
|
+
where(:reference_name => "owasp").select('DISTINCT value')
|
94
|
+
end
|
95
|
+
|
96
|
+
#
|
97
|
+
#
|
98
|
+
def cwe
|
99
|
+
where(:reference_name => "cwe").select('DISTINCT value')
|
100
|
+
end
|
101
|
+
end
|
9
102
|
end
|
10
103
|
end
|
11
104
|
end
|
@@ -1,7 +1,7 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
|
3
1
|
require 'risu'
|
4
2
|
|
3
|
+
ActiveRecord::Migration.verbose = false
|
4
|
+
|
5
5
|
module Risu
|
6
6
|
module Parsers
|
7
7
|
module Nessus
|
@@ -16,8 +16,13 @@ module Risu
|
|
16
16
|
#
|
17
17
|
def initialize
|
18
18
|
@vals = Hash.new
|
19
|
+
|
20
|
+
@valid_references = Array[
|
21
|
+
"cpe", "bid", "see_also", "xref", "cve", "iava", "msft",
|
22
|
+
"osvdb", "cert", "edb-id", "rhsa", "secunia", "suse", "dsa",
|
23
|
+
"owasp", "cwe"]
|
19
24
|
|
20
|
-
@valid_elements = Array["
|
25
|
+
@valid_elements = Array["ReportItem", "plugin_version", "risk_factor",
|
21
26
|
"description", "cvss_base_score", "solution", "item", "plugin_output", "tag", "synopsis", "plugin_modification_date",
|
22
27
|
"FamilyName", "FamilyItem", "Status", "vuln_publication_date", "ReportHost", "HostProperties", "preferenceName",
|
23
28
|
"preferenceValues", "preferenceType", "fullName", "pluginId", "pluginName", "selectedValue", "selectedValue",
|
@@ -26,8 +31,11 @@ module Risu
|
|
26
31
|
"Report", "Family", "Preferences", "PluginsPreferences", "FamilySelection", "IndividualPluginSelection", "PluginId",
|
27
32
|
"pci-dss-compliance", "exploitability_ease", "cvss_temporal_vector", "exploit_framework_core", "cvss_temporal_score",
|
28
33
|
"exploit_available", "metasploit_name", "exploit_framework_canvas", "canvas_package", "exploit_framework_metasploit",
|
29
|
-
"plugin_type", "
|
34
|
+
"plugin_type", "exploithub_sku", "exploit_framework_exploithub", "stig_severity", "plugin_name", "fname",
|
35
|
+
]
|
30
36
|
|
37
|
+
@valid_elements = @valid_elements + @valid_references
|
38
|
+
|
31
39
|
# This makes adding new host properties really easy, except for the
|
32
40
|
#MS patch numbers, this are handled differently.
|
33
41
|
@valid_host_properties = {
|
@@ -60,14 +68,15 @@ module Risu
|
|
60
68
|
"pcidss:dns_zone_transfer" => :pcidss_dns_zone_transfer,
|
61
69
|
"pcidss:unprotected_mssql_db" => :pcidss_unprotected_mssql_db,
|
62
70
|
"pcidss:obsolete_software" => :pcidss_obsolete_software,
|
63
|
-
"pcidss:www:sql_injection" => :pcidss_www_sql_injection
|
71
|
+
"pcidss:www:sql_injection" => :pcidss_www_sql_injection,
|
72
|
+
"fname" => :fname
|
64
73
|
}
|
65
74
|
end
|
66
75
|
|
67
76
|
# Callback for when the start of a xml element is reached
|
68
77
|
#
|
69
|
-
# @param element
|
70
|
-
# @param attributes
|
78
|
+
# @param element XML element
|
79
|
+
# @param attributes Attributes for the XML element
|
71
80
|
def on_start_element(element, attributes)
|
72
81
|
@tag = element
|
73
82
|
@vals[@tag] = ""
|
@@ -227,29 +236,17 @@ module Risu
|
|
227
236
|
end if @attr != nil
|
228
237
|
#We cannot handle the references in the same block as the rest of the ReportItem tag because
|
229
238
|
#there tends to be more than of the different types of reference per ReportItem, this causes issue for a sax
|
230
|
-
#parser. To solve this we do the references before the final plugin data
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
@
|
235
|
-
@
|
236
|
-
|
237
|
-
@
|
238
|
-
@bid.reference_name = "bid"
|
239
|
-
@bid.value = @vals["bid"]
|
240
|
-
@bid.save
|
241
|
-
when "see_also"
|
242
|
-
@see_also = @plugin.references.create
|
243
|
-
@see_also.reference_name = "see_also"
|
244
|
-
@see_also.value = @vals["see_also"]
|
245
|
-
@see_also.save
|
246
|
-
when "xref"
|
247
|
-
@xref = @plugin.references.create
|
248
|
-
@xref.reference_name = "xref"
|
249
|
-
@xref.value = @vals["xref"]
|
250
|
-
@xref.save
|
239
|
+
#parser. To solve this we do the references before the final plugin data, Valid references must be added
|
240
|
+
#the @valid_reference array at the top to be parsed.
|
241
|
+
# *@valid_reference, does a 'when' on each element of the @valid_references array, pure magic
|
242
|
+
when *@valid_references
|
243
|
+
@ref = @plugin.references.create
|
244
|
+
@ref.reference_name = element
|
245
|
+
@ref.value = @vals["#{element}"]
|
246
|
+
@ref.save
|
251
247
|
when "ReportItem"
|
252
248
|
@ri.plugin_output = @vals["plugin_output"]
|
249
|
+
@ri.plugin_name = @vals["plugin_name"]
|
253
250
|
@ri.save
|
254
251
|
|
255
252
|
@plugin.attributes = {
|
@@ -257,6 +254,7 @@ module Risu
|
|
257
254
|
:risk_factor => @vals["risk_factor"],
|
258
255
|
:description => @vals["description"],
|
259
256
|
:plugin_publication_date => @vals["plugin_publication_date"],
|
257
|
+
:plugin_modification_date => @vals["plugin_modification_date"],
|
260
258
|
:synopsis => @vals["synopsis"],
|
261
259
|
:plugin_type => @vals["plugin_type"],
|
262
260
|
:cvss_vector => @vals["cvss_vector"],
|
@@ -272,10 +270,10 @@ module Risu
|
|
272
270
|
:metasploit_name => @vals["metasploit_name"],
|
273
271
|
:exploit_framework_canvas => @vals["exploit_framework_canvas"],
|
274
272
|
:canvas_package => @vals["canvas_package"],
|
275
|
-
:cpe => @vals["cpe"],
|
276
273
|
:exploit_framework_exploithub => @vals["exploit_framework_exploithub"],
|
277
274
|
:exploithub_sku => @vals["exploithub_sku"],
|
278
|
-
:stig_severity => @vals["stig_severity"]
|
275
|
+
:stig_severity => @vals["stig_severity"],
|
276
|
+
:fname => @vals["fname"]
|
279
277
|
}
|
280
278
|
@plugin.save
|
281
279
|
end
|