oscal 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rake.yml +15 -0
  3. data/.github/workflows/release.yml +24 -0
  4. data/.gitignore +11 -0
  5. data/.gitmodules +3 -0
  6. data/.hound.yml +5 -0
  7. data/.rubocop.yml +10 -0
  8. data/Gemfile +2 -0
  9. data/README.adoc +63 -0
  10. data/bin/console +30 -0
  11. data/bin/setup +8 -0
  12. data/exe/convert2oscalyaml.rb +560 -0
  13. data/lib/oscal/add.rb +25 -0
  14. data/lib/oscal/address.rb +21 -0
  15. data/lib/oscal/address_line.rb +10 -0
  16. data/lib/oscal/alter.rb +21 -0
  17. data/lib/oscal/back_matter.rb +19 -0
  18. data/lib/oscal/base64_object.rb +10 -0
  19. data/lib/oscal/base_class.rb +49 -0
  20. data/lib/oscal/catalog.rb +50 -10
  21. data/lib/oscal/choice.rb +10 -0
  22. data/lib/oscal/citation.rb +21 -0
  23. data/lib/oscal/combine.rb +10 -0
  24. data/lib/oscal/common_utils.rb +35 -0
  25. data/lib/oscal/constraint.rb +19 -0
  26. data/lib/oscal/control.rb +20 -31
  27. data/lib/oscal/custom.rb +21 -0
  28. data/lib/oscal/document_id.rb +10 -0
  29. data/lib/oscal/email_address.rb +10 -0
  30. data/lib/oscal/exclude_control.rb +21 -0
  31. data/lib/oscal/external_id.rb +10 -0
  32. data/lib/oscal/group.rb +26 -35
  33. data/lib/oscal/guideline.rb +10 -0
  34. data/lib/oscal/hash_object.rb +10 -0
  35. data/lib/oscal/import_object.rb +21 -0
  36. data/lib/oscal/include_control.rb +21 -0
  37. data/lib/oscal/insert_control.rb +21 -0
  38. data/lib/oscal/link.rb +10 -0
  39. data/lib/oscal/location.rb +30 -0
  40. data/lib/oscal/location_uuid.rb +10 -0
  41. data/lib/oscal/matching.rb +10 -0
  42. data/lib/oscal/member_of_organization.rb +10 -0
  43. data/lib/oscal/merge.rb +19 -0
  44. data/lib/oscal/metadata_block.rb +28 -14
  45. data/lib/oscal/modify.rb +21 -0
  46. data/lib/oscal/parameter.rb +22 -20
  47. data/lib/oscal/part.rb +13 -22
  48. data/lib/oscal/party.rb +35 -0
  49. data/lib/oscal/party_uuid.rb +10 -0
  50. data/lib/oscal/profile.rb +32 -7
  51. data/lib/oscal/property.rb +3 -25
  52. data/lib/oscal/remove.rb +10 -0
  53. data/lib/oscal/resource.rb +28 -0
  54. data/lib/oscal/responsible_party.rb +23 -0
  55. data/lib/oscal/revision.rb +22 -0
  56. data/lib/oscal/rlink.rb +19 -0
  57. data/lib/oscal/role.rb +21 -0
  58. data/lib/oscal/select.rb +19 -0
  59. data/lib/oscal/serializer.rb +17 -4
  60. data/lib/oscal/set_parameter.rb +30 -0
  61. data/lib/oscal/telephone_number.rb +10 -0
  62. data/lib/oscal/test.rb +10 -0
  63. data/lib/oscal/url.rb +10 -0
  64. data/lib/oscal/value.rb +36 -0
  65. data/lib/oscal/version.rb +1 -1
  66. data/lib/oscal/with_id.rb +39 -0
  67. data/lib/oscal.rb +1 -13
  68. data/oscal.gemspec +9 -11
  69. data/spec/oscal/catalog_spec.rb +39 -0
  70. data/spec/oscal_spec.rb +7 -0
  71. data/spec/spec_helper.rb +15 -0
  72. metadata +65 -9
  73. data/lib/oscal/component.rb +0 -14
  74. data/lib/oscal/prose.rb +0 -13
  75. data/lib/oscal/statement.rb +0 -12
@@ -0,0 +1,560 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Confidential and proprietary trade secret material of Ribose, Inc.
4
+ # (c) 2023 Ribose, Inc. as unpublished work.
5
+ #
6
+ #
7
+ #
8
+ # This script attempts to read the ISO 27002:2022 controls and convert to
9
+ # OSCAL yaml file.
10
+
11
+ require "fileutils"
12
+ require "optparse"
13
+ require "ostruct"
14
+ require "yaml"
15
+ require "securerandom"
16
+ require "asciidoctor"
17
+ require "nokogiri"
18
+ require "json"
19
+ require "time"
20
+
21
+ def convert_control_to_oscal(control)
22
+ clause_no = control["identifier"].split(":")[-1]
23
+
24
+ # set control id
25
+ catalog_control = Catalog::Control.new("clause_#{clause_no}")
26
+ catalog_control.props = []
27
+ catalog_control.parts = []
28
+
29
+ control.each do |key, value|
30
+ case key
31
+ when "identifier"
32
+ # add control props
33
+ catalog_control.props << {
34
+ "name" => "clause",
35
+ "value" => clause_no,
36
+ }
37
+ when "maps_27002_2013"
38
+ catalog_control.props << {
39
+ "name" => "maps_27002_2013",
40
+ "value" => value,
41
+ }
42
+ when "title"
43
+ catalog_control.title = value
44
+ when "tags"
45
+ # add tags as props
46
+ value.each do |k, v|
47
+ catalog_control.props << {
48
+ "name" => k,
49
+ "value" => v,
50
+ }
51
+ end
52
+ when "control"
53
+ part = Catalog::Part.new
54
+ part.id = "control_#{clause_no}"
55
+ part.name = "clause_#{clause_no}_control"
56
+ part.prose = value.chomp
57
+ catalog_control.parts << part.to_hash
58
+ when "purpose"
59
+ part = Catalog::Part.new
60
+ part.id = "purpose_#{clause_no}"
61
+ part.name = "clause_#{clause_no}_purpose"
62
+ part.prose = value.chomp
63
+ catalog_control.parts << part.to_hash
64
+ when "other_info"
65
+ part = Catalog::Part.new
66
+ part.id = "other_info_#{clause_no}"
67
+ part.name = "clause_#{clause_no}_other_info"
68
+ unless value.empty?
69
+ part.parts = parse_content(
70
+ value,
71
+ clause_no,
72
+ "other_info",
73
+ )
74
+ end
75
+ catalog_control.parts << part.to_hash
76
+ when "guidance"
77
+ # guidance contains multiple parts with title and content
78
+ # each part may contains empty title
79
+ # each part contains content
80
+ # content may contains list or table
81
+ value.each do |v|
82
+ part = Catalog::Part.new
83
+ part.id = "scls_#{clause_no.gsub('.', '-')}"
84
+ part.name = if v["title"].nil?
85
+ "clause_#{clause_no}_guidance"
86
+ else
87
+ v["title"]
88
+ end
89
+ unless v["content"].empty?
90
+ part.parts = parse_content(
91
+ v["content"],
92
+ clause_no,
93
+ )
94
+ end
95
+ catalog_control.parts << part.to_hash
96
+ end
97
+ end
98
+ end
99
+
100
+ catalog_control
101
+ end
102
+
103
+ def add_note(nokogiri_children, _content, part)
104
+ note = nokogiri_children.css(".note").first.content
105
+ note = note.gsub("Note", "NOTE:")
106
+ note_part = Catalog::Part.new
107
+ note_part.id = "#{part.id}_note"
108
+ note_part.name = "#{part.name}_note"
109
+ note_part.prose = note
110
+
111
+ note_part
112
+ end
113
+
114
+ def replace_link(nokogiri_children, content)
115
+ nokogiri_children.xpath("//a/@href").each do |href|
116
+ href_value = href.value[1..-1]
117
+
118
+ clause_no = if href_value.start_with?("scls_")
119
+ href_value.split("_").last.gsub("-", ".")
120
+ else
121
+ href_value
122
+ end
123
+
124
+ content = content.gsub(
125
+ "[#{href_value}]",
126
+ "[#{clause_no}](##{href_value})",
127
+ )
128
+ end
129
+
130
+ content
131
+ end
132
+
133
+ def parse_content(content, clause_no, content_type = "guidance")
134
+ parts = []
135
+
136
+ # convert content into html
137
+ html = Asciidoctor.convert content, safe: :safe
138
+ # replace \n with space
139
+ html = html.gsub("\n", " ")
140
+ html = html.gsub("> <", "><")
141
+ # parse html by nokogiri
142
+ htmldoc = Nokogiri::HTML(html)
143
+ # get the body
144
+ body = htmldoc.children[1].children[0]
145
+
146
+ prev_part = nil
147
+ prev_second_part = nil
148
+ first_level_index = 0
149
+
150
+ body.children.each do |c|
151
+ next if c.content == " "
152
+
153
+ tag_name = c.name
154
+ css_classes = c.attr("class").split
155
+ is_olist = css_classes.include?("olist")
156
+ is_table = tag_name.match?("table")
157
+ contains_alink = c.search("a").count.positive?
158
+ contains_colon_at_end = c.content.chars.last(1).join == ":"
159
+ is_link_to_table = contains_alink &&
160
+ c.xpath("//a/@href")[0].value.match?("table")
161
+
162
+ if is_olist
163
+ # get all second level list items
164
+ list_items = c.css("ol[class=arabic] > li")
165
+ second_level_index = 0
166
+
167
+ list_items.each do |item|
168
+ part = Catalog::Part.new
169
+ part.id = "#{content_type}_#{clause_no}_part_#{first_level_index}_#{second_level_index + 1}"
170
+ part.name = "#{content_type}_part_list_item"
171
+ content = item.css("p").first.content
172
+
173
+ if item.search("a").count.positive?
174
+ content = replace_link(item, content)
175
+ end
176
+
177
+ if item.search(".note").count.positive?
178
+ note_part = add_note(item, content, part)
179
+ part.parts ||= []
180
+ part.parts << note_part
181
+ end
182
+
183
+ part.prose = content
184
+ prev_part.parts ||= []
185
+
186
+ # list item contains sublist items
187
+ if item.css("ol").count.positive?
188
+ prev_second_part = part
189
+ sublist_items = item.css("li")
190
+ third_level_index = 0
191
+
192
+ sublist_items.each do |sub_item|
193
+ sub_part = Catalog::Part.new
194
+ sub_part.id = "#{content_type}_#{clause_no}_part_#{first_level_index}_#{second_level_index + 1}_#{third_level_index + 1}"
195
+ sub_part.name = "#{content_type}_part_list_item"
196
+ content = sub_item.content
197
+
198
+ if sub_item.search("a").count.positive?
199
+ content = replace_link(sub_item, content)
200
+ end
201
+
202
+ sub_part.prose = content
203
+
204
+ prev_second_part.parts ||= []
205
+ prev_second_part.parts << sub_part
206
+
207
+ third_level_index = third_level_index + 1
208
+ end
209
+
210
+ part = prev_second_part
211
+ end
212
+
213
+ prev_part.parts << part
214
+
215
+ second_level_index = second_level_index + 1
216
+ end
217
+
218
+ first_level_index = first_level_index - 1
219
+ parts << prev_part
220
+ elsif is_table
221
+ column_num = 0
222
+ table_header = []
223
+ table_body = []
224
+ table_prose = ""
225
+
226
+ c.children.each do |item|
227
+ case item.name
228
+ when "caption"
229
+ part = Catalog::Part.new
230
+ part.id = "#{content_type}_#{clause_no}_part_#{first_level_index + 1}"
231
+ part.name = "#{content_type}_table_title"
232
+ part.prose = item.content
233
+ part.props = []
234
+ if item.content.match?(".")
235
+ part.props << {
236
+ "name" => "table",
237
+ "value" => item.content.split(".")[0].split[1],
238
+ }
239
+ end
240
+
241
+ parts << part.to_hash
242
+ when "colgroup"
243
+ column_num = item.children.count
244
+ when "thead"
245
+ item.children.children.each do |th|
246
+ # bold header
247
+ head = th.content.empty? ? " " : "*#{th.content}*"
248
+ table_header << head
249
+ end
250
+ when "tbody"
251
+ # loop through tr
252
+ item.children.each do |tr|
253
+ tr_data = []
254
+
255
+ # loop through td
256
+ tr.css("td").each do |td|
257
+ tr_data << td.content
258
+ end
259
+
260
+ table_body << tr_data
261
+ end
262
+ end
263
+ end
264
+
265
+ # create prose for table
266
+ table_prose = "|===\n"
267
+ table_prose << ("|#{table_header.join(' | ')}\n")
268
+ table_body.each do |tr|
269
+ # bold first column
270
+ tr[0] = "*#{tr[0]}*"
271
+ table_prose << ("|#{tr.join(' | ')}\n")
272
+ end
273
+ table_prose << "|==="
274
+
275
+ # create part for table
276
+ part = Catalog::Part.new
277
+ part.id = "#{content_type}_#{clause_no}_part_#{first_level_index + 1}"
278
+ part.name = "#{content_type}_table"
279
+ part.prose = table_prose
280
+
281
+ parts << part.to_hash
282
+ elsif contains_colon_at_end
283
+ part = Catalog::Part.new
284
+ part.id = "#{content_type}_#{clause_no}_part_#{first_level_index + 1}"
285
+ part.name = "#{content_type}_part"
286
+ content = c.content
287
+
288
+ if contains_alink
289
+ content = replace_link(c, content)
290
+ end
291
+
292
+ part.prose = content
293
+
294
+ prev_part = part
295
+ else
296
+ part = Catalog::Part.new
297
+ part.id = "#{content_type}_#{clause_no}_part_#{first_level_index + 1}"
298
+ part.name = "#{content_type}_part"
299
+ content = c.content
300
+
301
+ if contains_alink
302
+ content = replace_link(c, content)
303
+ end
304
+
305
+ if content.start_with?("Note ")
306
+ content = content.gsub("Note ", "NOTE: ")
307
+ end
308
+
309
+ part.prose = content
310
+
311
+ parts << part.to_hash
312
+ end
313
+
314
+ first_level_index = first_level_index + 1
315
+ end
316
+
317
+ parts
318
+ end
319
+
320
+ #
321
+ # https://dev.to/ayushn21/how-to-generate-yaml-from-ruby-objects-without-type-annotations-4fli
322
+ module Hashify
323
+ # Classes that include this module can exclude certain
324
+ # instance variable from its hash representation by overriding
325
+ # this method
326
+ def ivars_excluded_from_hash
327
+ []
328
+ end
329
+
330
+ def to_hash
331
+ hash = {}
332
+ excluded_ivars = ivars_excluded_from_hash
333
+
334
+ # Iterate over all the instance variables and store their
335
+ # names and values in a hash
336
+ instance_variables.each do |var|
337
+ next if excluded_ivars.include? var.to_s
338
+
339
+ value = instance_variable_get(var)
340
+ value = value.map(&:to_hash) if value.is_a? Array
341
+
342
+ hash[var.to_s.delete("@")] = value
343
+ end
344
+
345
+ hash
346
+ end
347
+ end
348
+
349
+ #########################################
350
+ #
351
+ # Catalog
352
+ #
353
+ # Catalog contains uuid, metadata and groups.
354
+ #
355
+ class Catalog
356
+ include Hashify
357
+
358
+ attr_accessor :catalog
359
+
360
+ def initialize(title = nil, remark = nil)
361
+ @catalog = Hash.new
362
+ @catalog["uuid"] = SecureRandom.uuid
363
+ @catalog["metadata"] = Metadata.build(title, remark)
364
+ @catalog["groups"] = []
365
+ @catalog["back-matter"] = BackMatter.build
366
+ end
367
+
368
+ def catalog_groups=(arr)
369
+ @catalog["groups"] = arr
370
+ end
371
+
372
+ def add_catalog_groups(arr)
373
+ @catalog["groups"] << arr
374
+ end
375
+
376
+ def get_catalog_groups(num)
377
+ @catalog["groups"].select { |g| g.id == num }.first
378
+ end
379
+
380
+ def hashify_catalog_groups
381
+ @catalog["groups"].map!(&:to_hash)
382
+ end
383
+
384
+ def catalog_groups
385
+ @catalog["groups"]
386
+ end
387
+
388
+ #
389
+ # BackMatter
390
+ #
391
+ # BackMatter contains title, published, last-modified, version, oscal-version
392
+ # and remarks
393
+ #
394
+ class BackMatter
395
+ include Hashify
396
+
397
+ def self.build
398
+ back_matter_path = File.expand_path(
399
+ "../sources/sections/back-matter.yml",
400
+ __dir__,
401
+ )
402
+ back_matter = YAML.safe_load(File.read(back_matter_path))
403
+
404
+ back_matter["resources"].each do |res|
405
+ res["uuid"] = SecureRandom.uuid.to_s
406
+ end
407
+
408
+ back_matter
409
+ end
410
+ end
411
+
412
+ #
413
+ # Metadata
414
+ #
415
+ # Metadata contains title, published, last-modified, version, oscal-version
416
+ # and remarks
417
+ #
418
+ class Metadata
419
+ include Hashify
420
+
421
+ def self.build(title = nil, remark = nil)
422
+ default_title = "Catalog for ISO27002:2022"
423
+ default_remarks = "OSCAL yaml generated from ISO27002:2022"
424
+
425
+ {
426
+ "title" => title.nil? ? default_title : "",
427
+ "published" => Time.now.iso8601,
428
+ "last-modified" => Time.now.iso8601,
429
+ "version" => "1.0",
430
+ "oscal-version" => "1.0.0",
431
+ "remarks" => remark.nil? ? default_remarks : "",
432
+ }
433
+ end
434
+ end
435
+
436
+ #
437
+ # Group
438
+ #
439
+ # Group has id and title.
440
+ # Group can contains mulitple controls or groups.
441
+ # Group can contains props in name-value pairs
442
+ #
443
+ class Group
444
+ include Hashify
445
+
446
+ attr_accessor :id, :title, :groups, :controls, :props
447
+
448
+ def initialize(id, title)
449
+ @id = id
450
+ @title = title
451
+ end
452
+ end
453
+
454
+ #
455
+ # Control
456
+ #
457
+ # Control has id and title.
458
+ # Control can contains mulitple parts.
459
+ # Control can contains props in name-value pairs
460
+ #
461
+ class Control
462
+ include Hashify
463
+
464
+ attr_accessor :id, :title, :parts, :props
465
+
466
+ def initialize(id)
467
+ @id = id
468
+ end
469
+ end
470
+
471
+ #
472
+ # Part
473
+ #
474
+ # Part has id and name
475
+ # Part may contains prose.
476
+ # Part can contains mulitple parts.
477
+ # Part can contains props in name-value pairs
478
+ #
479
+ class Part
480
+ include Hashify
481
+
482
+ attr_accessor :id, :name, :prose, :parts, :props
483
+ end
484
+ end
485
+
486
+ def get_group_num_and_name(control_path)
487
+ group_shortname = control_path.split("/")[-2]
488
+ group_num = control_path.split("/")[-1].split("-")[0]
489
+ groupname = ""
490
+
491
+ case group_shortname
492
+ when "controls-org"
493
+ groupname = "Organizational controls"
494
+ when "controls-people"
495
+ groupname = "People controls"
496
+ when "controls-physical"
497
+ groupname = "Physical controls"
498
+ when "controls-tech"
499
+ groupname = "Technological controls"
500
+ end
501
+
502
+ [group_num, groupname]
503
+ end
504
+
505
+ ########
506
+ # Main #
507
+ ########
508
+ oscal_catalog = Catalog.new
509
+ controls_sections_dir = File.expand_path("../sources/sections", __dir__)
510
+ controls_paths_files = Dir["#{controls_sections_dir}/**/paths.yml"]
511
+
512
+ # read controls based on paths.yml
513
+ controls_paths_files.sort.each do |controls_paths_file|
514
+ controls_paths = YAML.safe_load(File.read(controls_paths_file))
515
+ prev_groupnum = nil
516
+
517
+ controls_paths.each do |control_path|
518
+ groupnum = get_group_num_and_name(control_path)[0]
519
+ groupname = get_group_num_and_name(control_path)[1]
520
+
521
+ if prev_groupnum.nil? || groupnum != prev_groupnum
522
+ # create groups for sections, e.g. 5, 6, 7, 8
523
+ catalog_group = Catalog::Group.new("cls_#{groupnum}", groupname)
524
+ catalog_group.props = [
525
+ {
526
+ "name" => "clause",
527
+ "value" => groupnum,
528
+ },
529
+ ]
530
+
531
+ oscal_catalog.add_catalog_groups(catalog_group)
532
+ prev_groupnum = groupnum
533
+ else
534
+ # get existing group
535
+ catalog_group = oscal_catalog.get_catalog_groups("cls_#{groupnum}")
536
+ end
537
+
538
+ # DEBUG
539
+ # set control path to a specific yml file path
540
+ # if control_path == "sections/controls-<TYPE>/X-YY.yml"
541
+ # if control_path == "sections/controls-org/5-02.yml"
542
+ control = YAML.safe_load(File.read("./sources/#{control_path}"))
543
+ converted_control = convert_control_to_oscal(control)
544
+
545
+ catalog_group.controls ||= []
546
+ catalog_group.controls << converted_control.to_hash
547
+ # end
548
+ end
549
+ end
550
+
551
+ ##########
552
+ # Output #
553
+ ##########
554
+
555
+ # Remove type annotation from ruby objects
556
+ oscal_catalog.hashify_catalog_groups
557
+
558
+ File.write("scripts/output/iso27002-oscal.yml", oscal_catalog.to_hash.to_yaml)
559
+
560
+ exit 0
data/lib/oscal/add.rb ADDED
@@ -0,0 +1,25 @@
1
+ require_relative "base_class"
2
+
3
+ module Oscal
4
+ class Add < Oscal::BaseClass
5
+ KEY = %i(position by_id title params props links parts)
6
+
7
+ attr_accessor *KEY
8
+ attr_serializable *KEY
9
+
10
+ def set_value(key_name, val)
11
+ case key_name
12
+ when 'params'
13
+ Parameter.wrap(val)
14
+ when 'props'
15
+ Property.wrap(val)
16
+ when 'links'
17
+ Link.wrap(val)
18
+ when 'part'
19
+ Part.wrap(val)
20
+ else
21
+ val
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,21 @@
1
+ require_relative "base_class"
2
+
3
+ module Oscal
4
+ class Address < Oscal::BaseClass
5
+ KEY = %i(type addr_lines city state postal_code country)
6
+
7
+ attr_accessor *KEY
8
+ attr_serializable *KEY
9
+
10
+ def set_value(key_name, val)
11
+ case key_name
12
+ when 'addr_lines'
13
+ AddressLine.wrap(val)
14
+ when 'links'
15
+ Link.wrap(val)
16
+ else
17
+ val
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,10 @@
1
+ require_relative "base_class"
2
+
3
+ module Oscal
4
+ class AddressLine < Oscal::BaseClass
5
+ KEY = %i(val)
6
+
7
+ attr_accessor *KEY
8
+ attr_serializable *KEY
9
+ end
10
+ end
@@ -0,0 +1,21 @@
1
+ require_relative "base_class"
2
+
3
+ module Oscal
4
+ class Alter < Oscal::BaseClass
5
+ KEY = %i(control_id klass removes adds)
6
+
7
+ attr_accessor *KEY
8
+ attr_serializable *KEY
9
+
10
+ def set_value(key_name, val)
11
+ case key_name
12
+ when 'removes'
13
+ Remove.wrap(val)
14
+ when 'adds'
15
+ Add.wrap(val)
16
+ else
17
+ val
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ require_relative "base_class"
2
+
3
+ module Oscal
4
+ class BackMatter < Oscal::BaseClass
5
+ KEY = %i(resources)
6
+
7
+ attr_accessor *KEY
8
+ attr_serializable *KEY
9
+
10
+ def set_value(key_name, val)
11
+ case key_name
12
+ when 'resources'
13
+ Resource.wrap(val)
14
+ else
15
+ val
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,10 @@
1
+ require_relative "base_class"
2
+
3
+ module Oscal
4
+ class Base64Object < Oscal::BaseClass
5
+ KEY = %i(filename media_type value)
6
+
7
+ attr_accessor *KEY
8
+ attr_serializable *KEY
9
+ end
10
+ end