iodef 0.2.0 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +46 -13
- data/lib/iodef/model.rb +15 -7
- data/lib/iodef/version.rb +1 -1
- data/lib/iodef/xml/tree.rb +2 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7588549cdf3dc70ba42067341ab74cd8e9f9fb7d1ce51614017e9ae6f66d5e5f
|
|
4
|
+
data.tar.gz: 9154dbeb6a501db304bc654f4b5ae4031c8e8219a14355a1f7a2eaa176ce2a39
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1164739062dfa364d9ce1db9c6e4308d565399942134c63e6c0bbc890d35f40f07009f5d6e772e2ead5faa1d7b133a2f94be8ddfdc82d161a79a4058ca99e900
|
|
7
|
+
data.tar.gz: 4242987cd5815cf220ee61a9c9d1e77e7f84b15dcd81c93f7ae8982ef3664d59b9356502e3e2d664f4b5b311b5042282692dcd384707315701c7b1066fb2d132
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1 - 2026-10-12
|
|
4
|
+
|
|
5
|
+
- Select the correct description language when plain English strings appear with translated text, and inherit document and incident languages for multilingual objects in typed views.
|
|
6
|
+
- Reject malformed processing-instruction targets when parsing XML and serializing XML extensions.
|
|
7
|
+
|
|
3
8
|
## 0.2.0 - 2026-10-12
|
|
4
9
|
|
|
5
10
|
- Read and write RFC 7970 XML incident reports with `IODEF.parse_xml` and `Document#to_xml`, including namespace-aware extensions and multilingual text.
|
data/README.md
CHANGED
|
@@ -1,25 +1,47 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">IODEF</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">Incident reports in JSON, CBOR, and XML.</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://rubygems.org/gems/iodef"><img src="https://img.shields.io/gem/v/iodef" alt="Gem version"></a>
|
|
7
|
+
<a href="https://github.com/ydah/iodef/actions/workflows/main.yml"><img src="https://github.com/ydah/iodef/actions/workflows/main.yml/badge.svg?branch=main" alt="CI"></a>
|
|
8
|
+
<a href="https://www.ruby-lang.org/"><img src="https://img.shields.io/badge/Ruby-%3E%3D%203.3-CC342D?logo=ruby&logoColor=white" alt="Ruby 3.3 or later"></a>
|
|
9
|
+
<a href="LICENSE.txt"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT License"></a>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="#installation">Installation</a> ·
|
|
14
|
+
<a href="#quick-start">Quick start</a> ·
|
|
15
|
+
<a href="#scope">Scope</a> ·
|
|
16
|
+
<a href="#development">Development</a> ·
|
|
17
|
+
<a href="#license">License</a>
|
|
18
|
+
</p>
|
|
2
19
|
|
|
3
20
|
Read, validate, build, and exchange IODEF v2 incident reports as JSON, CBOR, or XML ([RFC 8727](https://www.rfc-editor.org/rfc/rfc8727.html) and [RFC 7970](https://www.rfc-editor.org/rfc/rfc7970.html)).
|
|
4
21
|
|
|
5
|
-
|
|
6
|
-
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- Build incident reports with typed models and multilingual descriptions.
|
|
25
|
+
- Exchange JSON, built-in CBOR, and namespace-aware RFC 7970 XML.
|
|
26
|
+
- Validate reports with JSON Pointer diagnostics and configurable resource limits.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
7
29
|
|
|
8
|
-
|
|
9
|
-
incident = document.incidents.first
|
|
10
|
-
incident.incident_id
|
|
11
|
-
incident.contacts.first.role
|
|
12
|
-
incident.description.text("ja")
|
|
30
|
+
Add the gem to your Gemfile:
|
|
13
31
|
|
|
14
|
-
|
|
15
|
-
|
|
32
|
+
```ruby
|
|
33
|
+
gem "iodef"
|
|
16
34
|
```
|
|
17
35
|
|
|
18
|
-
|
|
36
|
+
Then run `bundle install`.
|
|
37
|
+
|
|
38
|
+
Ruby 3.3 or newer, JRuby, and TruffleRuby are supported. Runtime dependencies are the Ruby standard libraries `json` (2.19.4 or newer, including 3.x) and `rexml` (3.4.4 or newer, below 4); no other gem from this project is required. `IODEF` and `Iodef` refer to the same module.
|
|
19
39
|
|
|
20
|
-
##
|
|
40
|
+
## Quick start
|
|
21
41
|
|
|
22
42
|
```ruby
|
|
43
|
+
require "iodef"
|
|
44
|
+
|
|
23
45
|
document = IODEF::Document.build(lang: "en") do |doc|
|
|
24
46
|
doc.add_incident(purpose: "reporting") do |incident|
|
|
25
47
|
incident.incident_id = { id: "case-24", name: "response.example" }
|
|
@@ -33,12 +55,19 @@ document = IODEF::Document.build(lang: "en") do |doc|
|
|
|
33
55
|
end
|
|
34
56
|
|
|
35
57
|
json = document.to_json
|
|
58
|
+
copy = IODEF.parse(json)
|
|
59
|
+
copy.incidents.first.incident_id
|
|
60
|
+
copy.incidents.first.contacts.first.role # => "creator"
|
|
61
|
+
copy.incidents.first.description.text("ja") # => "不審な通信"
|
|
62
|
+
IODEF.validate(json) # => []
|
|
36
63
|
```
|
|
37
64
|
|
|
38
65
|
Builders require the incident's ID, generation time, and at least one contact. They do not invent reporting identities or timestamps. Known properties use snake_case accessors; wire property names retain their RFC spelling. `Document`, `Incident`, `Contact`, `Assessment`, `EventData`, and `Indicator` have typed views. Other objects are ordinary hashes. Plural conveniences include `incidents`, `contacts`, `assessments`, and `indicators`.
|
|
39
66
|
|
|
40
67
|
Typed child views update their document. `[]` exposes the underlying value, `[]=` copies the assigned value, and `to_h` returns a detached copy. Symbols in builder hashes become strings. Unknown JSON properties are preserved, including nested values and large integers. Defaults such as restriction levels are not inserted into the source data.
|
|
41
68
|
|
|
69
|
+
`description.text(language)` selects an exact language match, then a matching language prefix. Plain JSON strings mean English. Multilingual objects without `lang` inherit the incident or document language; an empty `lang` resets that inheritance. With no match or no requested language, the accessor prefers the first plain string or object without an explicit language, then the first entry.
|
|
70
|
+
|
|
42
71
|
## Validation and limits
|
|
43
72
|
|
|
44
73
|
Parsing is strict by default. `IODEF.parse(json, strict: false)` returns a document with a `warnings` array of `Violation` objects. `validate: false` is a compatibility spelling for this inspection mode. `validate!` refreshes diagnostics after edits. Serialization always validates and raises `IODEF::ValidationError` with `violations` and the first JSON `pointer`.
|
|
@@ -92,7 +121,7 @@ Numeric literals use Ruby integers and floats. Other consumers that apply XML Sc
|
|
|
92
121
|
|
|
93
122
|
## Scope
|
|
94
123
|
|
|
95
|
-
| Supported
|
|
124
|
+
| Supported | Outside this release |
|
|
96
125
|
| --- | --- |
|
|
97
126
|
| JSON binding, all RFC 8727 class/property definitions | Ambiguous binary XML structures and vendor extension semantics |
|
|
98
127
|
| Typed core models, multilingual text, builders | RID or other exchange transports |
|
|
@@ -125,6 +154,10 @@ The fixture task verifies SHA-256-pinned RFC/IANA sources, retains source notice
|
|
|
125
154
|
|
|
126
155
|
`script/update_registry.rb` audits the pinned IANA metadata. `--refresh` downloads the current registry, rejects unexplained differences, and updates its checksum only after the audit succeeds. Review RFC errata and registry changes before refreshing. No downloaded reference material is tracked or included in the gem.
|
|
127
156
|
|
|
157
|
+
## Contributing
|
|
158
|
+
|
|
159
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/ydah/iodef/issues). See [Development](#development) for the checks to run before submitting a change. Report security vulnerabilities privately as described in [SECURITY.md](SECURITY.md).
|
|
160
|
+
|
|
128
161
|
## License
|
|
129
162
|
|
|
130
163
|
Project code is MIT licensed; see [LICENSE.txt](LICENSE.txt). Added library dependencies are limited to MIT-licensed libraries. Ruby implementations and standard/default libraries (including JSON and REXML) are exceptions and retain their own licenses. Downloaded standards material is test reference data under its source terms and is neither committed nor packaged. These statements do not relicense third-party material.
|
data/lib/iodef/model.rb
CHANGED
|
@@ -18,10 +18,11 @@ module Iodef
|
|
|
18
18
|
validate! if strict && !@warnings.empty?
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def self.view(data, limits)
|
|
21
|
+
def self.view(data, limits, parent: nil)
|
|
22
22
|
allocate.tap do |model|
|
|
23
23
|
model.instance_variable_set(:@data, data)
|
|
24
24
|
model.instance_variable_set(:@limits, limits)
|
|
25
|
+
model.instance_variable_set(:@parent, parent)
|
|
25
26
|
model.instance_variable_set(:@warnings, [].freeze)
|
|
26
27
|
end
|
|
27
28
|
end
|
|
@@ -58,7 +59,7 @@ module Iodef
|
|
|
58
59
|
end
|
|
59
60
|
|
|
60
61
|
def description
|
|
61
|
-
MultilingualText.new(@data.fetch("Description", []), lang:
|
|
62
|
+
MultilingualText.new(@data.fetch("Description", []), lang: effective_language)
|
|
62
63
|
end
|
|
63
64
|
|
|
64
65
|
def description=(value)
|
|
@@ -75,6 +76,12 @@ module Iodef
|
|
|
75
76
|
end
|
|
76
77
|
end
|
|
77
78
|
|
|
79
|
+
protected
|
|
80
|
+
|
|
81
|
+
def effective_language
|
|
82
|
+
@data.fetch("lang") { @parent&.effective_language }
|
|
83
|
+
end
|
|
84
|
+
|
|
78
85
|
private
|
|
79
86
|
|
|
80
87
|
def read_field(field, descriptor)
|
|
@@ -90,7 +97,7 @@ module Iodef
|
|
|
90
97
|
|
|
91
98
|
def wrap(value, type)
|
|
92
99
|
klass = TYPED[type]
|
|
93
|
-
klass && value.is_a?(Hash) ? klass.view(value, limits) : value
|
|
100
|
+
klass && value.is_a?(Hash) ? klass.view(value, limits, parent: self) : value
|
|
94
101
|
end
|
|
95
102
|
end
|
|
96
103
|
|
|
@@ -109,7 +116,7 @@ module Iodef
|
|
|
109
116
|
|
|
110
117
|
def add_incident(purpose:, **attributes)
|
|
111
118
|
value = { "purpose" => purpose }.merge(attributes.transform_keys(&:to_s))
|
|
112
|
-
incident = Incident.view(Iodef.copy(value, limits), limits)
|
|
119
|
+
incident = Incident.view(Iodef.copy(value, limits), limits, parent: self)
|
|
113
120
|
yield incident if block_given?
|
|
114
121
|
@data["Incident"] ||= []
|
|
115
122
|
@data["Incident"] << incident.instance_variable_get(:@data)
|
|
@@ -132,7 +139,7 @@ module Iodef
|
|
|
132
139
|
|
|
133
140
|
def add_contact(role:, type:, **attributes)
|
|
134
141
|
value = { "role" => role, "type" => type }.merge(attributes.transform_keys(&:to_s))
|
|
135
|
-
contact = Contact.view(Iodef.copy(value, limits), limits)
|
|
142
|
+
contact = Contact.view(Iodef.copy(value, limits), limits, parent: self)
|
|
136
143
|
yield contact if block_given?
|
|
137
144
|
@data["Contact"] ||= []
|
|
138
145
|
@data["Contact"] << contact.instance_variable_get(:@data)
|
|
@@ -162,11 +169,12 @@ module Iodef
|
|
|
162
169
|
|
|
163
170
|
# Exact matches win; a language prefix and then an unlabelled value are fallbacks.
|
|
164
171
|
def text(language = nil)
|
|
165
|
-
values = @values.map { |value| value.is_a?(String) ? { "value" => value, "lang" =>
|
|
172
|
+
values = @values.map { |value| value.is_a?(String) ? { "value" => value, "lang" => "en" } : value.merge("lang" => value.fetch("lang", @lang)) }
|
|
166
173
|
language = language&.downcase
|
|
167
174
|
selected = values.find { |value| value["lang"]&.downcase == language } if language
|
|
168
175
|
selected ||= values.find { |value| value["lang"].to_s.downcase.split("-").first == language.split("-").first } if language
|
|
169
|
-
|
|
176
|
+
fallback = @values.index { |value| value.is_a?(String) || value["lang"].nil? || value["lang"].empty? }
|
|
177
|
+
selected ||= values[fallback] if fallback
|
|
170
178
|
(selected || values.first)&.fetch("value", nil)
|
|
171
179
|
end
|
|
172
180
|
|
data/lib/iodef/version.rb
CHANGED
data/lib/iodef/xml/tree.rb
CHANGED
|
@@ -8,6 +8,7 @@ module Iodef
|
|
|
8
8
|
Node = Data.define(:name, :uri, :local, :attributes, :namespaces, :parts, :lang)
|
|
9
9
|
Markup = Data.define(:kind, :value)
|
|
10
10
|
ENTITIES = { "amp" => "&", "lt" => "<", "gt" => ">", "quot" => '"', "apos" => "'" }.freeze
|
|
11
|
+
PI_TARGET = /\A#{REXML::XMLTokens::NCNAME_STR}\z/u.freeze
|
|
11
12
|
|
|
12
13
|
def initialize(limits)
|
|
13
14
|
@limits = limits
|
|
@@ -91,6 +92,7 @@ module Iodef
|
|
|
91
92
|
when :comment
|
|
92
93
|
append(stack.last, Markup.new(:comment, event[0])) unless stack.empty?
|
|
93
94
|
when :processing_instruction
|
|
95
|
+
raise ParseError, "invalid XML processing instruction target" unless PI_TARGET.match?(event[0])
|
|
94
96
|
raise ParseError, "reserved XML processing instruction" if event[0].downcase == "xml"
|
|
95
97
|
|
|
96
98
|
append(stack.last, Markup.new(:instruction, [event[0], event[1]].join(" "))) unless stack.empty?
|