oscal 0.1.0 → 0.1.1

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.
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