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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: abb43889b6b8b21b5b16be80fd2ed74c92042692b725fce72e60e7ec3f6c8628
4
- data.tar.gz: 96c490a291b68bfbf8d527eba06c6b6f528600e797ac05372750fe29f6258c68
3
+ metadata.gz: 7588549cdf3dc70ba42067341ab74cd8e9f9fb7d1ce51614017e9ae6f66d5e5f
4
+ data.tar.gz: 9154dbeb6a501db304bc654f4b5ae4031c8e8219a14355a1f7a2eaa176ce2a39
5
5
  SHA512:
6
- metadata.gz: cd6942a46620066e97795ce412dde97d7c41cf278c81eacd971c357b1f99b0b131020d01ddac53e8d7f09800149c0d349a8c0256316c0cd94ab84a862444a37c
7
- data.tar.gz: fa4ffc07109c725c824cff336e86cc86321746472e5d9e1116dbaea15483b44e40485904612a975bc2e43969d04ae32545f26d153bc5fd689d46f4b76b10522f
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
- # IODEF
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&amp;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> &middot;
14
+ <a href="#quick-start">Quick start</a> &middot;
15
+ <a href="#scope">Scope</a> &middot;
16
+ <a href="#development">Development</a> &middot;
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
- ```ruby
6
- require "iodef"
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
- document = IODEF.parse(json)
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
- issues = IODEF.validate(json)
15
- issues.each { |issue| puts "#{issue.pointer}: #{issue.rule}: #{issue.message}" }
32
+ ```ruby
33
+ gem "iodef"
16
34
  ```
17
35
 
18
- Install with `gem install iodef`. 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.
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
- ## Build and edit a report
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 in 0.2.0 | Outside 0.2.0 |
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: @data["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" => @lang } : value }
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
- selected ||= values.find { |value| value["lang"].nil? || value["lang"].empty? }
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Iodef
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
5
5
  end
@@ -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?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: iodef
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yudai Takada