iodef 0.1.0 → 0.2.0
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/CHANGELOG.md +6 -0
- data/README.md +37 -18
- data/SECURITY.md +1 -1
- data/data/enums.yml +1 -1
- data/data/types.yml +1 -1
- data/lib/iodef/binary_cbor.rb +285 -0
- data/lib/iodef/cbor.rb +16 -23
- data/lib/iodef/cbor_guard.rb +9 -110
- data/lib/iodef/version.rb +1 -1
- data/lib/iodef/xml/codec.rb +363 -0
- data/lib/iodef/xml/tree.rb +229 -0
- data/lib/iodef/xml.rb +24 -0
- data/sig/iodef.rbs +2 -0
- metadata +28 -4
data/lib/iodef/cbor_guard.rb
CHANGED
|
@@ -1,123 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Iodef
|
|
4
|
-
#
|
|
4
|
+
# Validate the wire representation and resource bounds while decoding it once.
|
|
5
5
|
class CBORGuard
|
|
6
6
|
def initialize(bytes, limits)
|
|
7
7
|
@bytes = bytes
|
|
8
8
|
@limits = limits
|
|
9
|
-
@offset = 0
|
|
10
|
-
@nodes = 0
|
|
11
|
-
@preserved_tags = []
|
|
12
9
|
end
|
|
13
10
|
|
|
14
11
|
def validate!
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
|
|
26
|
-
def byte
|
|
27
|
-
result = @bytes.getbyte(@offset)
|
|
28
|
-
raise ParseError, "truncated CBOR input" unless result
|
|
29
|
-
|
|
30
|
-
@offset += 1
|
|
31
|
-
result
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def take(length)
|
|
35
|
-
raise ParseError, "truncated CBOR input" if length > @bytes.bytesize - @offset
|
|
36
|
-
|
|
37
|
-
start = @offset
|
|
38
|
-
@offset += length
|
|
39
|
-
@bytes.byteslice(start, length)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def argument(info)
|
|
43
|
-
return info if info < 24
|
|
44
|
-
return nil if info == 31
|
|
45
|
-
|
|
46
|
-
width = { 24 => 1, 25 => 2, 26 => 4, 27 => 8 }[info]
|
|
47
|
-
raise ParseError, "reserved CBOR additional information" unless width
|
|
48
|
-
|
|
49
|
-
take(width).unpack1({ 1 => "C", 2 => "n", 4 => "N", 8 => "Q>" }.fetch(width))
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def item(depth)
|
|
53
|
-
@nodes += 1
|
|
54
|
-
raise LimitError, "CBOR exceeds max_nodes" if @nodes > @limits[:max_nodes]
|
|
55
|
-
raise LimitError, "CBOR exceeds max_depth" if depth > @limits[:max_depth]
|
|
56
|
-
|
|
57
|
-
start = @offset
|
|
58
|
-
initial = byte
|
|
59
|
-
major = initial >> 5
|
|
60
|
-
info = initial & 31
|
|
61
|
-
length = argument(info)
|
|
62
|
-
case major
|
|
63
|
-
when 0, 1
|
|
64
|
-
raise ParseError, "indefinite CBOR integer" unless length
|
|
65
|
-
when 2, 3 then string(major, length, depth)
|
|
66
|
-
when 4, 5 then collection(major, length, depth)
|
|
67
|
-
when 6
|
|
68
|
-
raise ParseError, "indefinite CBOR tag" unless length
|
|
69
|
-
raise ConversionError, "unsupported CBOR tag" unless [0, 2, 3, 22, 23, 32].include?(length)
|
|
70
|
-
|
|
71
|
-
@preserved_tags << [start, @offset - start, { 2 => 253, 3 => 254, 32 => 255 }.fetch(length)] if [2, 3, 32].include?(length)
|
|
72
|
-
item(depth + 1)
|
|
73
|
-
when 7
|
|
74
|
-
raise ParseError, "unexpected CBOR break" if info == 31
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def string(major, length, depth)
|
|
79
|
-
if length
|
|
80
|
-
take(length)
|
|
81
|
-
raise LimitError, "binary exceeds max_binary_size" if major == 2 && length > @limits[:max_binary_size]
|
|
82
|
-
|
|
83
|
-
length
|
|
84
|
-
else
|
|
85
|
-
total = 0
|
|
86
|
-
until @bytes.getbyte(@offset) == 255
|
|
87
|
-
initial = @bytes.getbyte(@offset)
|
|
88
|
-
raise ParseError, "invalid indefinite CBOR string chunk" unless initial && (initial >> 5) == major && (initial & 31) < 28
|
|
89
|
-
|
|
90
|
-
total += item(depth + 1)
|
|
91
|
-
raise LimitError, "binary exceeds max_binary_size" if major == 2 && total > @limits[:max_binary_size]
|
|
92
|
-
end
|
|
93
|
-
byte
|
|
94
|
-
total
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def collection(major, length, depth)
|
|
99
|
-
raise LimitError, "CBOR exceeds max_collection_size" if length && length > @limits[:max_collection_size]
|
|
100
|
-
|
|
101
|
-
count = 0
|
|
102
|
-
keys = {}
|
|
103
|
-
until length ? count == length : @bytes.getbyte(@offset) == 255
|
|
104
|
-
count += 1
|
|
105
|
-
raise LimitError, "CBOR exceeds max_collection_size" if count > @limits[:max_collection_size]
|
|
106
|
-
|
|
107
|
-
if major == 5
|
|
108
|
-
start = @offset
|
|
109
|
-
initial = @bytes.getbyte(start)
|
|
110
|
-
raise ConversionError, "CBOR map keys must be text or integers" unless initial && [0, 1, 3].include?(initial >> 5)
|
|
111
|
-
|
|
112
|
-
item(depth + 1)
|
|
113
|
-
key = CBOR.decode(@bytes.byteslice(start, @offset - start))
|
|
114
|
-
raise ParseError, "duplicate CBOR map key" if keys.key?(key)
|
|
115
|
-
|
|
116
|
-
keys[key] = true
|
|
117
|
-
end
|
|
118
|
-
item(depth + 1)
|
|
119
|
-
end
|
|
120
|
-
byte unless length
|
|
12
|
+
BinaryCBOR.decode(@bytes, **@limits.slice(:max_input_size, :max_depth, :max_nodes, :max_collection_size, :max_binary_size),
|
|
13
|
+
max_string_size: @limits[:max_input_size], allowed_tags: [0, 2, 3, 22, 23, 32])
|
|
14
|
+
rescue BinaryCBOR::LimitError => e
|
|
15
|
+
raise LimitError, e.message
|
|
16
|
+
rescue BinaryCBOR::UnsupportedError => e
|
|
17
|
+
raise ConversionError, e.message
|
|
18
|
+
rescue BinaryCBOR::Error => e
|
|
19
|
+
raise ParseError, e.message
|
|
121
20
|
end
|
|
122
21
|
end
|
|
123
22
|
end
|
data/lib/iodef/version.rb
CHANGED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Iodef
|
|
4
|
+
class XMLCodec
|
|
5
|
+
NS = "urn:ietf:params:xml:ns:iodef-2.0"
|
|
6
|
+
SCI = "urn:ietf:params:xml:ns:iodef-sci-1.0"
|
|
7
|
+
ENUM = "urn:ietf:params:xml:ns:iodef-enum-1.0"
|
|
8
|
+
DS = "http://www.w3.org/2000/09/xmldsig#"
|
|
9
|
+
XSI = "http://www.w3.org/2001/XMLSchema-instance"
|
|
10
|
+
TEXT = { "IncidentID" => "id", "IndicatorID" => "id", "RegistryHandle" => "handle",
|
|
11
|
+
"Address" => "value", "Counter" => "value", "TimeImpact" => "value", "MonetaryImpact" => "value",
|
|
12
|
+
"Confidence" => "value", "RecordPattern" => "value", "MLString" => "value",
|
|
13
|
+
"Extension" => "value", "SoftwareReference" => "value" }.freeze
|
|
14
|
+
WRAPPERS = { "Incident" => { "Indicator" => "IndicatorData" }, "EventData" => { "System" => "Flow", "RecordData" => "Record" },
|
|
15
|
+
"Service" => { "ApplicationHeaderField" => "ApplicationHeader" },
|
|
16
|
+
"EmailData" => { "Signature" => "SignatureData" }, "File" => { "Signature" => "SignatureData" } }.freeze
|
|
17
|
+
IMPACTS = %w[SystemImpact BusinessImpact TimeImpact MonetaryImpact IntendedImpact].freeze
|
|
18
|
+
OPAQUE = %w[Signature X509Data DigestMethod CanonicalizationMethod].freeze
|
|
19
|
+
STRUCTURED = %w[StructuredInformation Platform Scoring].freeze
|
|
20
|
+
# XML has ordered children; JSON property insertion order carries no such meaning.
|
|
21
|
+
ORDER = {
|
|
22
|
+
"Document" => %w[Incident AdditionalData],
|
|
23
|
+
"Incident" => %w[IncidentID AlternativeID RelatedActivity DetectTime StartTime EndTime RecoveryTime ReportTime GenerationTime Description Discovery Assessment Method Contact EventData Indicator History AdditionalData],
|
|
24
|
+
"RelatedActivity" => %w[IncidentID URL ThreatActor Campaign IndicatorID Confidence Description AdditionalData],
|
|
25
|
+
"ThreatActor" => %w[ThreatActorID URL Description AdditionalData], "Campaign" => %w[CampaignID URL Description AdditionalData],
|
|
26
|
+
"Contact" => %w[ContactName ContactTitle Description RegistryHandle PostalAddress Email Telephone Timezone Contact AdditionalData],
|
|
27
|
+
"PostalAddress" => %w[PAddress Description], "Email" => %w[EmailTo Description], "Telephone" => %w[TelephoneNumber Description],
|
|
28
|
+
"DetectionPattern" => %w[Application Description DetectionConfiguration], "Method" => %w[Reference Description AttackPattern Vulnerability Weakness AdditionalData],
|
|
29
|
+
"Reference" => %w[ReferenceName URL Description], "Assessment" => %w[IncidentCategory Impact Counter MitigatingFactor Cause Confidence AdditionalData],
|
|
30
|
+
"HistoryItem" => %w[DateTime IncidentID Contact Description DefinedCOA AdditionalData],
|
|
31
|
+
"EventData" => %w[Description DetectTime StartTime EndTime RecoveryTime ReportTime Contact Discovery Assessment Method System Expectation RecordData EventData AdditionalData],
|
|
32
|
+
"Expectation" => %w[Description DefinedCOA StartTime EndTime Contact],
|
|
33
|
+
"System" => %w[Node NodeRole Service OperatingSystem Counter AssetID Description AdditionalData],
|
|
34
|
+
"Service" => %w[ServiceName Port Portlist ProtoType ProtoCode ProtoField ApplicationHeaderField EmailData Application],
|
|
35
|
+
"ServiceName" => %w[IANAService URL Description],
|
|
36
|
+
"RecordData" => %w[DateTime Description Application RecordPattern RecordItem URL FileData WindowsRegistryKeysModified CertificateData AdditionalData],
|
|
37
|
+
"Certificate" => %w[X509Data Description], "File" => %w[FileName FileSize FileType URL HashData Signature AssociatedSoftware FileProperties],
|
|
38
|
+
"FuzzyHash" => %w[FuzzyHashValue Application AdditionalData],
|
|
39
|
+
"Indicator" => %w[IndicatorID AlternativeIndicatorID Description StartTime EndTime Confidence Contact Observable uid-ref IndicatorExpression IndicatorReference NodeRole AttackPhase Reference AdditionalData],
|
|
40
|
+
"BulkObservable" => %w[BulkObservableFormat BulkObservableList AdditionalData], "BulkObservableFormat" => %w[Hash AdditionalData],
|
|
41
|
+
"IndicatorExpression" => %w[IndicatorExpression Observable uid-ref IndicatorReference Confidence AdditionalData],
|
|
42
|
+
"AttackPhase" => %w[AttackPhaseID URL Description AdditionalData], "Software" => %w[SoftwareReference URL Description],
|
|
43
|
+
"StructuredInformation" => %w[RawData Reference Platform Scoring], "Platform" => %w[RawData Reference], "Scoring" => %w[RawData Reference]
|
|
44
|
+
}.freeze
|
|
45
|
+
|
|
46
|
+
def initialize(limits)
|
|
47
|
+
@limits = limits
|
|
48
|
+
@tree = XMLTree.new(limits)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def decode(root)
|
|
52
|
+
expect_name(root, NS, "IODEF-Document")
|
|
53
|
+
read_object(root, "Document")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def encode(data)
|
|
57
|
+
writer = XMLTree::Writer.new(@limits)
|
|
58
|
+
writer.append('<?xml version="1.0" encoding="UTF-8"?>')
|
|
59
|
+
write_object(writer, "IODEF-Document", data, "Document", nil)
|
|
60
|
+
output = writer.value
|
|
61
|
+
# The same structural limits apply to output, including embedded XML fragments.
|
|
62
|
+
@tree.parse(output)
|
|
63
|
+
output
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def fields(type)
|
|
69
|
+
SCHEMA.fetch(type).fetch("fields")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def descriptor(type, field)
|
|
73
|
+
fields(type).fetch(field).delete_suffix("!")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def attribute?(type, field)
|
|
77
|
+
return false if TEXT[type] == field || (%w[Indicator IndicatorExpression].include?(type) && field == "uid-ref")
|
|
78
|
+
|
|
79
|
+
field.match?(/\A[a-z]/) || (STRUCTURED.include?(type) && %w[SpecID ContentID].include?(field))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def attributes(node)
|
|
83
|
+
node.attributes.reject { |key, _| key == "xmlns" || key.start_with?("xmlns:") }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def children(node)
|
|
87
|
+
node.parts.grep(XMLTree::Node)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def text(node)
|
|
91
|
+
raise ConversionError, "nested XML has no scalar JSON representation" unless children(node).empty?
|
|
92
|
+
|
|
93
|
+
node.parts.grep(String).join
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def element_content(node)
|
|
97
|
+
raise ConversionError, "mixed XML content has no JSON object representation" unless node.parts.grep(String).all? { |item| item.strip.empty? }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def expect_name(node, uri, name)
|
|
101
|
+
raise ConversionError, "unsupported XML element or namespace" unless node.uri == uri && node.local == name
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def read_object(node, type)
|
|
105
|
+
if node.uri == SCI && node.local == "AttackPattern" && children(node).any? { |child| child.uri == SCI && child.local == "Scoring" }
|
|
106
|
+
raise ConversionError, "AttackPattern cannot contain XML Scoring"
|
|
107
|
+
end
|
|
108
|
+
if STRUCTURED.include?(type) && children(node).any? { |child| child.local == "Reference" }
|
|
109
|
+
raise ConversionError, "SCI Reference uses an incompatible IODEF namespace"
|
|
110
|
+
end
|
|
111
|
+
data = {}
|
|
112
|
+
attributes(node).each do |name, value|
|
|
113
|
+
uri, local = @tree.expand(name, node.namespaces, attribute: true)
|
|
114
|
+
next if uri == XSI && %w[schemaLocation noNamespaceSchemaLocation].include?(local)
|
|
115
|
+
|
|
116
|
+
field = uri == XMLTree::XML_NS && local == "lang" ? "lang" : local
|
|
117
|
+
unless (uri.empty? || (uri == XMLTree::XML_NS && local == "lang")) && fields(type).key?(field) && attribute?(type, field) &&
|
|
118
|
+
(field != "lang" || uri == XMLTree::XML_NS)
|
|
119
|
+
raise ConversionError, "XML attribute has no JSON representation"
|
|
120
|
+
end
|
|
121
|
+
raise ConversionError, "duplicate mapped XML attribute" if data.key?(field)
|
|
122
|
+
|
|
123
|
+
data[field] = scalar(value, descriptor(type, field))
|
|
124
|
+
end
|
|
125
|
+
if TEXT.key?(type)
|
|
126
|
+
field = TEXT.fetch(type)
|
|
127
|
+
if %w[Extension SoftwareReference].include?(type) && data["dtype"] == "xml"
|
|
128
|
+
data[field] = fragment(node)
|
|
129
|
+
else
|
|
130
|
+
value = text(node)
|
|
131
|
+
if %w[Extension SoftwareReference].include?(type) && %w[byte file bytes frame packet ipv4-packet ipv6-packet].include?(data["dtype"])
|
|
132
|
+
value = value.gsub(/[\t\r\n ]/, "")
|
|
133
|
+
end
|
|
134
|
+
data[field] = scalar(value, descriptor(type, field)) unless value.empty? && !fields(type)[field].end_with?("!")
|
|
135
|
+
end
|
|
136
|
+
else
|
|
137
|
+
element_content(node)
|
|
138
|
+
children(node).each { |child| read_child(data, child, type) }
|
|
139
|
+
end
|
|
140
|
+
data["type"] ||= "unknown" if %w[SystemImpact BusinessImpact].include?(type)
|
|
141
|
+
data["category"] ||= "ipv6-addr" if type == "Address"
|
|
142
|
+
data
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def read_child(data, child, type)
|
|
146
|
+
wrapper = WRAPPERS.fetch(type, {}).key(child.local) if child.uri == NS
|
|
147
|
+
if wrapper
|
|
148
|
+
raise ConversionError, "wrapper attributes cannot be flattened without loss" unless attributes(child).empty?
|
|
149
|
+
element_content(child)
|
|
150
|
+
raise ConversionError, "empty XML wrapper" if children(child).empty?
|
|
151
|
+
|
|
152
|
+
children(child).each do |nested|
|
|
153
|
+
expect_name(nested, namespace(type, wrapper), xml_name(type, wrapper))
|
|
154
|
+
add(data, wrapper, read_value(nested, descriptor(type, wrapper).delete_suffix("[]"), wrapper), type)
|
|
155
|
+
end
|
|
156
|
+
return
|
|
157
|
+
end
|
|
158
|
+
if type == "Assessment" && child.uri == NS && IMPACTS.include?(child.local)
|
|
159
|
+
impact_type = child.local == "IntendedImpact" ? "BusinessImpact" : child.local
|
|
160
|
+
add(data, "Impact", { child.local => read_object(child, impact_type) }, type)
|
|
161
|
+
return
|
|
162
|
+
end
|
|
163
|
+
if %w[Indicator IndicatorExpression].include?(type) && child.uri == NS && child.local == "ObservableReference"
|
|
164
|
+
raise ConversionError, "unsupported ObservableReference attributes" unless attributes(child).keys == ["uid-ref"]
|
|
165
|
+
raise ConversionError, "ObservableReference must be empty" unless text(child).strip.empty?
|
|
166
|
+
|
|
167
|
+
add(data, "uid-ref", scalar(child.attributes["uid-ref"], "id"), type)
|
|
168
|
+
return
|
|
169
|
+
end
|
|
170
|
+
field = fields(type).keys.find { |candidate| !attribute?(type, candidate) && xml_name(type, candidate) == child.local && namespace(type, candidate) == child.uri }
|
|
171
|
+
raise ConversionError, "XML child has no JSON representation" unless field && field != TEXT[type]
|
|
172
|
+
raise ConversionError, "XML child requires its wrapper" if WRAPPERS.fetch(type, {}).key?(field)
|
|
173
|
+
|
|
174
|
+
add(data, field, read_value(child, descriptor(type, field).delete_suffix("[]"), field), type)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def read_value(node, type, field)
|
|
178
|
+
raise ConversionError, "XML cryptographic structures have no unambiguous JSON byte mapping" if OPAQUE.include?(field)
|
|
179
|
+
|
|
180
|
+
if type == "mlstring"
|
|
181
|
+
data = read_object(node, "MLString")
|
|
182
|
+
data["lang"] ||= node.lang if node.lang
|
|
183
|
+
return data["value"] if (data.keys - %w[value lang]).empty? && [nil, "en"].include?(data["lang"])
|
|
184
|
+
|
|
185
|
+
data
|
|
186
|
+
elsif SCHEMA.key?(type)
|
|
187
|
+
read_object(node, type)
|
|
188
|
+
elsif field == "RawData"
|
|
189
|
+
raise ConversionError, "RawData attributes have no JSON representation" unless attributes(node) == { "dtype" => "xml" }
|
|
190
|
+
element_content(node)
|
|
191
|
+
raise ConversionError, "RawData must contain one XML document" unless children(node).size == 1
|
|
192
|
+
|
|
193
|
+
bytes = @tree.serialize(children(node).first)
|
|
194
|
+
raise LimitError, "RawData exceeds max_binary_size" if bytes.bytesize > @limits[:max_binary_size]
|
|
195
|
+
|
|
196
|
+
[bytes].pack("m0")
|
|
197
|
+
else
|
|
198
|
+
raise ConversionError, "scalar XML attributes have no JSON representation" unless attributes(node).empty?
|
|
199
|
+
|
|
200
|
+
scalar(text(node), type)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def add(data, field, value, type)
|
|
205
|
+
if descriptor(type, field).end_with?("[]")
|
|
206
|
+
values = data[field] ||= []
|
|
207
|
+
raise LimitError, "collection exceeds max_collection_size" if values.size >= @limits[:max_collection_size]
|
|
208
|
+
|
|
209
|
+
values << value
|
|
210
|
+
else
|
|
211
|
+
raise ConversionError, "repeated XML element has no singular JSON representation" if data.key?(field)
|
|
212
|
+
|
|
213
|
+
data[field] = value
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def scalar(value, type)
|
|
218
|
+
case type
|
|
219
|
+
when "integer"
|
|
220
|
+
raise ConversionError, "invalid XML integer" unless value.strip.match?(/\A[+-]?[0-9]+\z/)
|
|
221
|
+
|
|
222
|
+
Integer(value.strip, 10)
|
|
223
|
+
when "number", "positive"
|
|
224
|
+
number = value.strip
|
|
225
|
+
raise ConversionError, "invalid XML number" unless number.match?(/\A[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?\z/)
|
|
226
|
+
|
|
227
|
+
result = number.match?(/\A[+-]?[0-9]+\z/) ? Integer(number, 10) : Float(number)
|
|
228
|
+
raise ConversionError, "non-finite XML number" if result.is_a?(Float) && !result.finite?
|
|
229
|
+
|
|
230
|
+
result
|
|
231
|
+
when "id", "language", "datetime", "uri", "timezone", "portlist" then value.strip.gsub(/[\t\r\n ]+/, " ")
|
|
232
|
+
when "binary" then value.gsub(/[\t\r\n ]/, "")
|
|
233
|
+
else ENUMS.key?(type) ? value.strip.gsub(/[\t\r\n ]+/, " ") : value
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def fragment(node)
|
|
238
|
+
writer = XMLTree::Writer.new(@limits)
|
|
239
|
+
node.parts.each do |part|
|
|
240
|
+
case part
|
|
241
|
+
when XMLTree::Node then @tree.write_node(writer, part)
|
|
242
|
+
when XMLTree::Markup
|
|
243
|
+
writer.append(part.kind == :comment ? "<!--#{part.value}-->" : "<?#{part.value}?>")
|
|
244
|
+
else writer.text(part)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
writer.value
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def namespace(type, field)
|
|
251
|
+
return DS if OPAQUE.include?(field) || (type == "Digest" && field == "DigestValue")
|
|
252
|
+
return ENUM if field == "ReferenceName" || type == "ReferenceName"
|
|
253
|
+
return SCI if %w[AttackPattern Vulnerability Weakness Platform Scoring RawData].include?(field)
|
|
254
|
+
|
|
255
|
+
NS
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def xml_name(type, field)
|
|
259
|
+
return "Nameservers" if type == "DomainData" && field == "NameServers"
|
|
260
|
+
return "Value" if type == "Key" && field == "KeyValue"
|
|
261
|
+
|
|
262
|
+
field
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def write_object(writer, name, data, type, language, extra_attributes = {})
|
|
266
|
+
raise ConversionError, "unknown JSON property has no XML representation" unless (data.keys - fields(type).keys).empty?
|
|
267
|
+
raise ConversionError, "AttackPattern cannot contain XML Scoring" if name == "AttackPattern" && data.key?("Scoring")
|
|
268
|
+
raise ConversionError, "SCI Reference uses an incompatible IODEF namespace" if STRUCTURED.include?(type) && data.key?("Reference")
|
|
269
|
+
|
|
270
|
+
attrs = extra_attributes.dup
|
|
271
|
+
attrs["xmlns"] = NS if type == "Document"
|
|
272
|
+
data.each do |field, value|
|
|
273
|
+
next unless attribute?(type, field)
|
|
274
|
+
|
|
275
|
+
attrs[field == "lang" ? "xml:lang" : field] = type == "Document" && field == "version" ? "2.00" : value
|
|
276
|
+
end
|
|
277
|
+
language = data.fetch("lang", language)
|
|
278
|
+
writer.open(name, attrs)
|
|
279
|
+
if TEXT.key?(type)
|
|
280
|
+
value = data[TEXT.fetch(type)]
|
|
281
|
+
if value
|
|
282
|
+
if %w[Extension SoftwareReference].include?(type) && data["dtype"] == "xml"
|
|
283
|
+
write_fragment(writer, value)
|
|
284
|
+
else
|
|
285
|
+
writer.text(value)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
else
|
|
289
|
+
order = ORDER.fetch(type) { fields(type).keys.reject { |field| attribute?(type, field) } }
|
|
290
|
+
order.each do |field|
|
|
291
|
+
next unless data.key?(field)
|
|
292
|
+
|
|
293
|
+
write_field(writer, field, data[field], type, language)
|
|
294
|
+
end
|
|
295
|
+
omitted = data.keys.reject { |field| attribute?(type, field) || order.include?(field) }
|
|
296
|
+
raise ConversionError, "JSON property has no ordered XML representation" unless omitted.empty?
|
|
297
|
+
end
|
|
298
|
+
writer.close(name)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def write_field(writer, field, value, type, language)
|
|
302
|
+
values = descriptor(type, field).end_with?("[]") ? value : [value]
|
|
303
|
+
wrapper = WRAPPERS.fetch(type, {})[field]
|
|
304
|
+
writer.open(wrapper) if wrapper
|
|
305
|
+
values.each do |item|
|
|
306
|
+
if type == "Assessment" && field == "Impact"
|
|
307
|
+
raise ConversionError, "unknown Impact property has no XML representation" unless item.size == 1
|
|
308
|
+
|
|
309
|
+
impact, content = item.first
|
|
310
|
+
write_object(writer, impact, content, impact == "IntendedImpact" ? "BusinessImpact" : impact, language)
|
|
311
|
+
elsif %w[Indicator IndicatorExpression].include?(type) && field == "uid-ref"
|
|
312
|
+
writer.open("ObservableReference", { "uid-ref" => item })
|
|
313
|
+
writer.close("ObservableReference")
|
|
314
|
+
else
|
|
315
|
+
write_value(writer, field, item, descriptor(type, field).delete_suffix("[]"), type, language)
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
writer.close(wrapper) if wrapper
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def write_value(writer, field, value, type, parent, language)
|
|
322
|
+
raise ConversionError, "XML cryptographic structures have no unambiguous JSON byte mapping" if OPAQUE.include?(field)
|
|
323
|
+
|
|
324
|
+
uri = namespace(parent, field)
|
|
325
|
+
name = xml_name(parent, field)
|
|
326
|
+
extra = {}
|
|
327
|
+
extra["xmlns"] = uri if uri != NS || STRUCTURED.include?(parent)
|
|
328
|
+
if type == "mlstring"
|
|
329
|
+
content = value.is_a?(String) ? { "value" => value } : value
|
|
330
|
+
extra["xml:lang"] = "en" if value.is_a?(String) && language && language != "en"
|
|
331
|
+
write_object(writer, name, content, "MLString", language, extra)
|
|
332
|
+
elsif SCHEMA.key?(type)
|
|
333
|
+
write_object(writer, name, value, type, language, extra)
|
|
334
|
+
elsif field == "RawData"
|
|
335
|
+
bytes = value.unpack1("m0")
|
|
336
|
+
node = @tree.parse(bytes)
|
|
337
|
+
writer.open(name, extra.merge("dtype" => "xml"))
|
|
338
|
+
@tree.write_node(writer, node)
|
|
339
|
+
writer.close(name)
|
|
340
|
+
else
|
|
341
|
+
writer.open(name, extra)
|
|
342
|
+
writer.text(value)
|
|
343
|
+
writer.close(name)
|
|
344
|
+
end
|
|
345
|
+
rescue ParseError
|
|
346
|
+
raise ConversionError, "embedded XML is not representable"
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def write_fragment(writer, value)
|
|
350
|
+
root = @tree.parse("<fragment>#{value}</fragment>")
|
|
351
|
+
root.parts.each do |part|
|
|
352
|
+
case part
|
|
353
|
+
when XMLTree::Node then @tree.write_node(writer, part)
|
|
354
|
+
when XMLTree::Markup
|
|
355
|
+
writer.append(part.kind == :comment ? "<!--#{part.value}-->" : "<?#{part.value}?>")
|
|
356
|
+
else writer.text(part)
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
rescue ParseError
|
|
360
|
+
raise ConversionError, "embedded XML is not representable"
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|