iodef 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ec203460113e786d9571db8360e79b7514881fbf38a2af7539813cdf4608069c
4
+ data.tar.gz: accc3826848d943da9d860d91d7889099fdad3c3141771737d0315c6d9da98af
5
+ SHA512:
6
+ metadata.gz: 775da0539aa19b31c1b914ac29e6bd7426cd594dcbcac2ab9c5b0d4a3b74681457d7d9f67cb0cf85e40fddae0121787fb1cc1ce4dd641133139e4b1dbbd42ed8
7
+ data.tar.gz: b770d06ee184c30f732d6ff69c9b367584915008e3d1cfba2562c8cc9a33a86cdb0a7e4190c910ec26a6c9956572bc517c63293ece3cb98226a5a3e045afefca
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-10-12
4
+
5
+ Initial release.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Yudai Takada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # IODEF
2
+
3
+ Read, validate, build, and exchange incident reports using the JSON binding of IODEF v2 ([RFC 8727](https://www.rfc-editor.org/rfc/rfc8727.html), with the information model in [RFC 7970](https://www.rfc-editor.org/rfc/rfc7970.html)).
4
+
5
+ ```ruby
6
+ require "iodef"
7
+
8
+ document = IODEF.parse(json)
9
+ incident = document.incidents.first
10
+ incident.incident_id
11
+ incident.contacts.first.role
12
+ incident.description.text("ja")
13
+
14
+ issues = IODEF.validate(json)
15
+ issues.each { |issue| puts "#{issue.pointer}: #{issue.rule}: #{issue.message}" }
16
+ ```
17
+
18
+ Install with `gem install iodef`. Ruby 3.3 or newer, JRuby, and TruffleRuby are supported. The only required runtime gem is `json` (2.19.4 or newer, including 3.x); no other gem from this project is required. `IODEF` and `Iodef` refer to the same module.
19
+
20
+ ## Build and edit a report
21
+
22
+ ```ruby
23
+ document = IODEF::Document.build(lang: "en") do |doc|
24
+ doc.add_incident(purpose: "reporting") do |incident|
25
+ incident.incident_id = { id: "case-24", name: "response.example" }
26
+ incident.generation_time = "2026-10-01T09:00:00Z"
27
+ incident.description = ["Unexpected traffic", { value: "不審な通信", lang: "ja" }]
28
+ incident.add_contact(role: "creator", type: "organization") do |contact|
29
+ contact.contact_name = ["Response team"]
30
+ contact.email = [{ "EmailTo" => "response@response.example" }]
31
+ end
32
+ end
33
+ end
34
+
35
+ json = document.to_json
36
+ ```
37
+
38
+ 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
+
40
+ 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
+
42
+ ## Validation and limits
43
+
44
+ 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`.
45
+
46
+ Required properties, types, nonempty collections, enum values, extension selector pairs, identifiers/references, dates, binary encodings, and information-model conditions are checked. Unknown properties remain available as extensions; their semantics are not inferred. Validation never fetches referenced URLs, schemas, or other network resources. XML, JSON, or regex text embedded in a report is not executed. JSON-valued extension strings are checked for JSON syntax.
47
+
48
+ | Option | Default |
49
+ | --- | ---: |
50
+ | `max_input_size` | 16 MiB of input/output; also bounds copied string data |
51
+ | `max_depth` | 32 nested levels |
52
+ | `max_collection_size` | 10,000 members per array or object |
53
+ | `max_nodes` | 200,000 JSON/CBOR values |
54
+ | `max_binary_size` | 1 MiB per decoded binary value |
55
+ | `max_violations` | 1,000 diagnostics |
56
+
57
+ Pass overrides to `parse`, `validate`, `parse_cbor`, or model constructors. Limits are positive integers. Resource-limit failures raise `LimitError` even in inspection mode. Malformed/duplicate-key JSON raises `ParseError`; cycles, nonfinite numbers, invalid string encodings, and non-JSON Ruby objects are rejected. Diagnostics identify properties and rules without including incident values.
58
+
59
+ ## Optional CBOR
60
+
61
+ Install the optional `cbor` gem (`~> 0.5.10`), then:
62
+
63
+ ```ruby
64
+ require "iodef/cbor"
65
+ bytes = document.to_cbor
66
+ copy = IODEF.parse_cbor(bytes)
67
+ ```
68
+
69
+ All 207 numeric map keys are supported. The encoder uses date tag 0, base64 byte-string tag 22, hexadecimal byte-string tag 23, and URI tag 32. Dates and URI spelling are preserved. Typed byte content becomes binary; hex output uses uppercase. The decoder also accepts contextually untagged strings/bytes, as used in the RFC's own CBOR examples. Integer bignums are preserved. IODEF real values are encoded as float32; values that would round or overflow raise `ConversionError`. Unknown JSON floats keep their original precision.
70
+
71
+ Unknown CBOR tags, unassigned integer keys, and opaque byte strings without an IODEF type cannot be represented unambiguously in JSON and raise `ConversionError`. Duplicate/colliding keys, excessive nesting, large collections, and trailing data are rejected before native decoding. The optional native backend is tested on CRuby 3.3–4.0 and TruffleRuby. JRuby and CRuby 4.1 development builds support the JSON API; the current `cbor` extension uses a C API removed in CRuby 4.1 and cannot be built there.
72
+
73
+ ## Scope
74
+
75
+ | Supported in 0.1.0 | Outside 0.1.0 |
76
+ | --- | --- |
77
+ | JSON binding, all RFC 8727 class/property definitions | RFC 7970 XML import/export |
78
+ | Typed core models, multilingual text, builders | RID or other exchange transports |
79
+ | IODEF enum extensions and preserved unknown JSON fields | Vendor extension semantics |
80
+ | Optional CBOR, numeric keys, typed binary values | Signing, encryption, threat correlation |
81
+ | RBS signatures and bounded validation | Authenticating senders or verifying referenced evidence |
82
+
83
+ The implementation has independent, MIT-licensed property metadata in `data/`. It does not embed CDDL, JSON Schema, RFC examples, IANA source downloads, or third-party implementation code. CDDL is a development-only conformance oracle fetched into ignored `tmp/`; it is not a runtime dependency.
84
+
85
+ ### Specification differences
86
+
87
+ RFC 8727's CDDL has a verified missing-brace correction ([erratum 7334](https://www.rfc-editor.org/errata/eid7334)). The generated test oracle applies it. Its section 4.2 JSON example needs presentation-wrapped strings joined; the original text is retained beside the corrected fixture. Both complete JSON and CBOR examples are tested.
88
+
89
+ The JSON examples use version `2.0`; the XML information model uses `2.00`. Both spellings are accepted and preserved. Required nonempty child groups and conditional values follow RFC 7970: nonnumeric `Confidence` omits `value`, numeric confidence requires it; a node can include both addresses and domain data; `HashData` supports `ext-scope`. `RelatedActivity.Description` accepts multilingual strings. These constraints are not all accurately expressed in the RFC 8727 CDDL. The section 2.2.5 illustration uses a CWE `SpecID` absent from its normative enum: use `private` with `ext-SpecID` for that extension.
90
+
91
+ The pinned IANA registry and RFC have differing spellings for `breach-propietary` / `breach-proprietary`, `anonymization` / `anomyzation`, and `ntp-stamp` / `ntpstamp`; both are accepted. RFC 8727 additionally defines the `json` extension data type. This library validates format and structure, not whether a reported address, software identifier, or incident fact is authentic.
92
+
93
+ ## Development
94
+
95
+ ```sh
96
+ bundle install
97
+ bundle exec rake test
98
+ COVERAGE=1 bundle exec rake test
99
+ bundle exec rubocop
100
+ bundle exec rbs -I sig validate
101
+ bundle exec ruby script/interop.rb
102
+ bundle exec ruby script/benchmark.rb 5000
103
+ ```
104
+
105
+ The fixture task verifies SHA-256-pinned RFC/IANA sources, retains source notices, and regenerates examples under ignored `tmp/rfc/`. Interoperability checks validate the two complete examples against downloaded CDDL and exchange CBOR with a pinned Python `cbor2` implementation, also kept under ignored `tmp/` with its license. The property table is checked against every CDDL object definition, and tests cover malformed values, enum extensions, limits, randomized round trips, and numeric precision.
106
+
107
+ `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.
108
+
109
+ ## License
110
+
111
+ MIT. See [LICENSE.txt](LICENSE.txt). Optional dependencies and temporary development references retain their own licenses.
data/SECURITY.md ADDED
@@ -0,0 +1,7 @@
1
+ # Security
2
+
3
+ Report suspected vulnerabilities privately through [GitHub's vulnerability reporting page](https://github.com/ydah/iodef/security/advisories/new). Include the gem version, Ruby engine, and a small synthetic reproducer. Do not include operational incident reports, personal data, credentials, or private indicators in public issues.
4
+
5
+ IODEF documents contain untrusted, potentially sensitive content. This library validates representation and selected information-model rules; it does not authenticate reports or enforce disclosure restrictions. Apply authorization, transport encryption, retention policies, and trusted source selection in the calling application. Do not execute embedded scripts, XML, regexes, or extension payloads. The library itself never fetches document-supplied URLs or schemas.
6
+
7
+ Size, depth, node, collection, binary, and diagnostic limits apply by default. Increase them only for known workloads. The optional native CBOR backend should be kept updated; only the IODEF tag set is allowed before it receives input. Invalid or ambiguous CBOR values fail instead of silently dropping information.
data/data/enums.yml ADDED
@@ -0,0 +1,40 @@
1
+ # Protocol identifiers, maintained independently from RFC source code and schemas.
2
+ restriction: [public, partner, need-to-know, private, default, white, green, amber, red, ext-value]
3
+ purpose: [traceback, mitigation, reporting, watch, other, ext-value]
4
+ status: [new, in-progress, forwarded, resolved, future, ext-value]
5
+ contact_role: [creator, reporter, admin, tech, provider, user, billing, legal, irt, abuse, cc, cc-irt, leo, vendor, vendor-support, victim, victim-notified, ext-value]
6
+ contact_type: [person, organization, ext-value]
7
+ registry: [internic, apnic, arin, lacnic, ripe, afrinic, local, ext-value]
8
+ postal_type: [street, mailing, ext-value]
9
+ email_type: [direct, hotline, ext-value]
10
+ telephone_type: [wired, mobile, fax, hotline, ext-value]
11
+ discovery_source: [nidps, hips, siem, av, third-party-monitoring, incident, os-log, application-log, device-log, network-flow, passive-dns, investigation, audit, internal-notification, external-notification, leo, partner, actor, unknown, ext-value]
12
+ spec_id: ['urn:ietf:params:xml:ns:mile:mmdef:1.2', private]
13
+ occurrence: [actual, potential]
14
+ severity: [low, medium, high]
15
+ completion: [failed, succeeded]
16
+ system_impact: [takeover-account, takeover-service, takeover-system, cps-manipulation, cps-damage, availability-data, availability-account, availability-service, availability-system, damaged-system, damaged-data, breach-proprietary, breach-propietary, breach-privacy, breach-credential, breach-configuration, integrity-data, integrity-configuration, integrity-hardware, traffic-redirection, monitoring-traffic, monitoring-host, policy, unknown, ext-value]
17
+ business_impact: [breach-proprietary, breach-privacy, breach-credential, loss-of-integrity, loss-of-service, theft-financial, theft-service, degraded-reputation, asset-damage, asset-manipulation, legal, extortion, unknown, ext-value]
18
+ business_severity: [none, low, medium, high, unknown, ext-value]
19
+ time_metric: [labor, elapsed, downtime, ext-value]
20
+ duration: [second, minute, hour, day, month, quarter, year, ext-value]
21
+ rating: [low, medium, high, numeric, unknown, ext-value]
22
+ action: [nothing, contact-source-site, contact-target-site, contact-sender, investigate, block-host, block-network, block-port, rate-limit-host, rate-limit-network, rate-limit-port, redirect-traffic, honeypot, upgrade-software, rebuild-asset, harden-asset, remediate-other, status-triage, status-new-info, watch-and-report, training, defined-coa, other, ext-value]
23
+ system_category: [source, target, intermediate, sensor, infrastructure, ext-value]
24
+ yes_no_unknown: ['yes', 'no', unknown]
25
+ ownership: [organization, personal, partner, customer, no-relationship, unknown, ext-value]
26
+ address_category: [asn, atm, e-mail, ipv4-addr, ipv4-net, ipv4-net-masked, ipv4-net-mask, ipv6-addr, ipv6-net, ipv6-net-masked, mac, site-uri, ext-value]
27
+ node_category: [client, client-enterprise, client-partner, client-remote, client-kiosk, client-mobile, server-internal, server-public, www, mail, webmail, messaging, streaming, voice, file, ftp, p2p, name, directory, credential, print, application, database, backup, dhcp, assessment, source-control, config-management, monitoring, infra, infra-firewall, infra-router, infra-switch, camera, proxy, remote-access, log, virtualization, pos, scada, scada-supervisory, sinkhole, honeypot, anomyzation, anonymization, c2-server, malware-distribution, drop-server, hop-point, reflector, phishing-site, spear-phishing-site, recruiting-site, fraudulent-site, ext-value]
28
+ counter_type: [count, peak, average, ext-value]
29
+ counter_unit: [byte, mbit, packet, flow, session, alert, message, event, host, site, organization, ext-value]
30
+ system_status: [spoofed, fraudulent, innocent-hacked, innocent-hijacked, unknown, ext-value]
31
+ domain_status: [reservedDelegation, assignedAndActive, assignedAndInactive, assignedAndOnHold, revoked, transferPending, registryLock, registrarLock, other, unknown, ext-value]
32
+ pattern_type: [regex, binary, xpath, ext-value]
33
+ offset_unit: [line, byte, ext-value]
34
+ registry_action: [add-key, add-value, delete-key, delete-value, modify-key, modify-value, ext-value]
35
+ hash_scope: [file-contents, file-pe-section, file-pe-iat, file-pe-resource, file-pdf-object, email-hash, email-headers-hash, email-body-hash, ext-value]
36
+ bulk_type: [asn, atm, e-mail, ipv4-addr, ipv4-net, ipv4-net-mask, ipv6-addr, ipv6-net, ipv6-net-mask, mac, site-uri, domain-name, domain-to-ipv4, domain-to-ipv6, domain-to-ipv4-timestamp, domain-to-ipv6-timestamp, ipv4-port, ipv6-port, windows-reg-key, file-hash, email-x-mailer, email-subject, http-user-agent, http-request-uri, mutex, file-path, user-name, ext-value]
37
+ operator: [not, and, or, xor]
38
+ software_spec: [custom, cpe, swid, ext-value]
39
+ software_dtype: [bytes, integer, real, string, xml, ext-value]
40
+ dtype: [boolean, byte, bytes, character, date-time, ntpstamp, ntp-stamp, integer, portlist, real, string, file, path, frame, packet, ipv4-packet, json, ipv6-packet, url, csv, winreg, xml, ext-value]
data/data/mapkeys.yml ADDED
@@ -0,0 +1,5 @@
1
+ # RFC 8727 section 5 wire identifiers, in integer order starting at -24.
2
+ first: -24
3
+ names: >-
4
+ version lang format-id private-enum-name private-enum-id Incident AdditionalData value translation-id name dtype ext-dtype meaning formatid restriction ext-restriction observable-id SoftwareReference URL Description spec-name ext-spec-name purpose ext-purpose
5
+ status ext-status IncidentID AlternativeID RelatedActivity DetectTime StartTime EndTime RecoveryTime ReportTime GenerationTime Discovery Assessment Method Contact EventData Indicator History id instance ThreatActor Campaign IndicatorID Confidence ThreatActorID CampaignID role ext-role type ext-type ContactName ContactTitle RegistryHandle PostalAddress Email Telephone Timezone handle registry ext-registry PAddress EmailTo TelephoneNumber source ext-source DetectionPattern DetectionConfiguration Application Reference AttackPattern Vulnerability Weakness SpecID ext-SpecID ContentID RawData Platform Scoring ReferenceName specIndex ID occurrence IncidentCategory Impact SystemImpact BusinessImpact TimeImpact MonetaryImpact IntendedImpact Counter MitigatingFactor Cause severity completion ext-severity metric ext-metric duration ext-duration currency rating ext-rating HistoryItem action ext-action DateTime DefinedCOA System Expectation RecordData category ext-category interface spoofed virtual ownership ext-ownership Node NodeRole Service OperatingSystem AssetID DomainData Address Location vlan-name vlan-num unit ext-unit system-status ext-system-status domain-status ext-domain-status Name DateDomainWasChecked RegistrationDate ExpirationDate RelatedDNS NameServers DomainContacts Server SameDomainContact ip-protocol ServiceName Port Portlist ProtoCode ProtoType ProtoField ApplicationHeaderField EmailData IANAService EmailFrom EmailSubject EmailX-Mailer EmailHeaderField EmailHeaders EmailBody EmailMessage HashData Signature RecordPattern RecordItem FileData WindowsRegistryKeysModified CertificateData offset offsetunit ext-offsetunit Key registryaction ext-registryaction KeyName KeyValue Certificate X509Data File FileName FileSize FileType AssociatedSoftware FileProperties scope HashTargetID Hash FuzzyHash DigestMethod DigestValue CanonicalizationMethod FuzzyHashValue AlternativeIndicatorID Observable uid-ref IndicatorExpression IndicatorReference AttackPhase BulkObservable BulkObservableFormat BulkObservableList operator ext-operator euid-ref AttackPhaseID
data/data/types.yml ADDED
@@ -0,0 +1,282 @@
1
+ # Independently maintained implementation metadata. Sections refer to RFC 7970;
2
+ # JSON cardinalities and property spellings follow RFC 8727 sections 2 and 3.
3
+ # A trailing ! requires a property; [] denotes a nonempty list when present.
4
+ groups:
5
+ restricted: {restriction: restriction, ext-restriction: string}
6
+ observable: {observable-id: id}
7
+ described: {Description: 'mlstring[]'}
8
+ extensible: {AdditionalData: 'Extension[]'}
9
+ temporal: {DetectTime: datetime, StartTime: datetime, EndTime: datetime, RecoveryTime: datetime, ReportTime: datetime}
10
+ linked: {URL: 'uri[]'}
11
+ structured: {SpecID: 'spec_id!', ext-SpecID: string, ContentID: string, RawData: 'binary[]', Reference: 'Reference[]'}
12
+ types:
13
+ Document:
14
+ section: '3.1'
15
+ groups: [extensible]
16
+ fields: {version: 'version!', lang: language, format-id: string, private-enum-name: string, private-enum-id: string, Incident: 'Incident[]!'}
17
+ Incident:
18
+ section: '3.2'
19
+ groups: [restricted, observable, described, extensible, temporal]
20
+ fields: {purpose: 'purpose!', ext-purpose: string, status: status, ext-status: string, lang: language, IncidentID: 'IncidentID!', AlternativeID: AlternativeID, RelatedActivity: 'RelatedActivity[]', GenerationTime: 'datetime!', Discovery: 'Discovery[]', Assessment: 'Assessment[]', Method: 'Method[]', Contact: 'Contact[]!', EventData: 'EventData[]', Indicator: 'Indicator[]', History: History}
21
+ IncidentID:
22
+ section: '3.4'
23
+ groups: [restricted]
24
+ fields: {id: 'string!', name: 'string!', instance: string}
25
+ AlternativeID:
26
+ section: '3.5'
27
+ groups: [restricted]
28
+ fields: {IncidentID: 'IncidentID[]!'}
29
+ RelatedActivity:
30
+ section: '3.6'
31
+ groups: [restricted, described, extensible, linked]
32
+ fields: {IncidentID: 'IncidentID[]', ThreatActor: 'ThreatActor[]', Campaign: 'Campaign[]', IndicatorID: 'IndicatorID[]', Confidence: Confidence}
33
+ at_least_one: [IncidentID, URL, ThreatActor, Campaign, IndicatorID, Confidence, Description, AdditionalData]
34
+ ThreatActor:
35
+ section: '3.7'
36
+ groups: [restricted, described, extensible, linked]
37
+ fields: {ThreatActorID: 'string[]'}
38
+ at_least_one: [ThreatActorID, URL, Description, AdditionalData]
39
+ Campaign:
40
+ section: '3.8'
41
+ groups: [restricted, described, extensible, linked]
42
+ fields: {CampaignID: 'string[]'}
43
+ at_least_one: [CampaignID, URL, Description, AdditionalData]
44
+ Contact:
45
+ section: '3.9'
46
+ groups: [restricted, described, extensible]
47
+ fields: {role: 'contact_role!', ext-role: string, type: 'contact_type!', ext-type: string, ContactName: 'mlstring[]', ContactTitle: 'mlstring[]', RegistryHandle: 'RegistryHandle[]', PostalAddress: 'PostalAddress[]', Email: 'Email[]', Telephone: 'Telephone[]', Timezone: timezone, Contact: 'Contact[]'}
48
+ at_least_one: [ContactName, ContactTitle, Description, RegistryHandle, PostalAddress, Email, Telephone, Timezone, Contact, AdditionalData]
49
+ RegistryHandle:
50
+ section: '3.9.1'
51
+ fields: {handle: 'string!', registry: 'registry!', ext-registry: string}
52
+ PostalAddress:
53
+ section: '3.9.2'
54
+ groups: [described]
55
+ fields: {type: postal_type, ext-type: string, PAddress: 'mlstring!'}
56
+ Email:
57
+ section: '3.9.3'
58
+ groups: [described]
59
+ fields: {type: email_type, ext-type: string, EmailTo: 'string!'}
60
+ Telephone:
61
+ section: '3.9.4'
62
+ groups: [described]
63
+ fields: {type: telephone_type, ext-type: string, TelephoneNumber: 'string!'}
64
+ Discovery:
65
+ section: '3.10'
66
+ groups: [restricted, described]
67
+ fields: {source: discovery_source, ext-source: string, Contact: 'Contact[]', DetectionPattern: 'DetectionPattern[]'}
68
+ DetectionPattern:
69
+ section: '3.10.1'
70
+ groups: [restricted, observable, described]
71
+ fields: {DetectionConfiguration: 'string[]', Application: 'Software!'}
72
+ at_least_one: [Description, DetectionConfiguration]
73
+ Method:
74
+ section: '3.11'
75
+ groups: [restricted, described, extensible]
76
+ fields: {Reference: 'Reference[]', AttackPattern: 'StructuredInformation[]', Vulnerability: 'StructuredInformation[]', Weakness: 'StructuredInformation[]'}
77
+ at_least_one: [Reference, Description, AttackPattern, Vulnerability, Weakness, AdditionalData]
78
+ Reference:
79
+ section: '3.11.1'
80
+ groups: [observable, described, linked]
81
+ fields: {ReferenceName: ReferenceName}
82
+ at_least_one: [ReferenceName, URL, Description]
83
+ ReferenceName:
84
+ section: '3.11.1.1'
85
+ fields: {specIndex: 'integer!', ID: 'id!'}
86
+ Assessment:
87
+ section: '3.12'
88
+ groups: [restricted, observable, extensible]
89
+ fields: {occurrence: occurrence, IncidentCategory: 'mlstring[]', Impact: 'Impact[]!', Counter: 'Counter[]', MitigatingFactor: 'mlstring[]', Cause: 'mlstring[]', Confidence: Confidence}
90
+ Impact:
91
+ section: '3.12'
92
+ fields: {SystemImpact: SystemImpact, BusinessImpact: BusinessImpact, TimeImpact: TimeImpact, MonetaryImpact: MonetaryImpact, IntendedImpact: BusinessImpact}
93
+ exactly_one: [SystemImpact, BusinessImpact, TimeImpact, MonetaryImpact, IntendedImpact]
94
+ SystemImpact:
95
+ section: '3.12.1'
96
+ groups: [described]
97
+ fields: {type: 'system_impact!', ext-type: string, severity: severity, completion: completion}
98
+ BusinessImpact:
99
+ section: '3.12.2'
100
+ groups: [described]
101
+ fields: {type: 'business_impact!', ext-type: string, severity: business_severity, ext-severity: string}
102
+ TimeImpact:
103
+ section: '3.12.3'
104
+ fields: {value: 'positive!', severity: severity, metric: 'time_metric!', ext-metric: string, duration: duration, ext-duration: string}
105
+ MonetaryImpact:
106
+ section: '3.12.4'
107
+ fields: {value: 'positive!', severity: severity, currency: string}
108
+ Confidence:
109
+ section: '3.12.5'
110
+ fields: {rating: 'rating!', ext-rating: string, value: number}
111
+ History:
112
+ section: '3.13'
113
+ groups: [restricted]
114
+ fields: {HistoryItem: 'HistoryItem[]!'}
115
+ HistoryItem:
116
+ section: '3.13.1'
117
+ groups: [restricted, observable, described, extensible]
118
+ fields: {action: 'action!', ext-action: string, DateTime: 'datetime!', IncidentID: IncidentID, Contact: Contact, DefinedCOA: 'string[]'}
119
+ EventData:
120
+ section: '3.14'
121
+ groups: [restricted, observable, described, extensible, temporal]
122
+ fields: {Contact: 'Contact[]', Discovery: 'Discovery[]', Assessment: Assessment, Method: 'Method[]', System: 'System[]', Expectation: 'Expectation[]', RecordData: 'RecordData[]', EventData: 'EventData[]'}
123
+ at_least_one: [Description, DetectTime, StartTime, EndTime, RecoveryTime, ReportTime, Contact, Discovery, Assessment, Method, System, Expectation, RecordData, EventData, AdditionalData]
124
+ Expectation:
125
+ section: '3.15'
126
+ groups: [restricted, observable, described]
127
+ fields: {action: action, ext-action: string, severity: severity, DefinedCOA: 'string[]', StartTime: datetime, EndTime: datetime, Contact: Contact}
128
+ System:
129
+ section: '3.17'
130
+ groups: [restricted, observable, described, extensible]
131
+ fields: {category: system_category, ext-category: string, interface: string, spoofed: yes_no_unknown, virtual: yes_no_unknown, ownership: ownership, ext-ownership: string, Node: 'Node!', NodeRole: 'NodeRole[]', Service: 'Service[]', OperatingSystem: 'Software[]', Counter: 'Counter[]', AssetID: 'string[]'}
132
+ Node:
133
+ section: '3.18'
134
+ fields: {DomainData: 'DomainData[]', Address: 'Address[]', PostalAddress: PostalAddress, Location: 'mlstring[]', Counter: 'Counter[]'}
135
+ at_least_one: [DomainData, Address]
136
+ Address:
137
+ section: '3.18.1'
138
+ groups: [observable]
139
+ fields: {value: 'string!', category: 'address_category!', ext-category: string, vlan-name: string, vlan-num: integer}
140
+ NodeRole:
141
+ section: '3.18.2'
142
+ groups: [described]
143
+ fields: {category: 'node_category!', ext-category: string}
144
+ Counter:
145
+ section: '3.18.3'
146
+ fields: {value: 'number!', type: 'counter_type!', ext-type: string, unit: 'counter_unit!', ext-unit: string, meaning: string, duration: duration, ext-duration: string}
147
+ DomainData:
148
+ section: '3.19'
149
+ groups: [observable]
150
+ fields: {system-status: 'system_status!', ext-system-status: string, domain-status: 'domain_status!', ext-domain-status: string, Name: 'string!', DateDomainWasChecked: datetime, RegistrationDate: datetime, ExpirationDate: datetime, RelatedDNS: 'Extension[]', NameServers: 'NameServers[]', DomainContacts: DomainContacts}
151
+ NameServers:
152
+ section: '3.19.1'
153
+ fields: {Server: 'string!', Address: 'Address[]!'}
154
+ DomainContacts:
155
+ section: '3.19.2'
156
+ fields: {SameDomainContact: string, Contact: 'Contact[]'}
157
+ exactly_one: [SameDomainContact, Contact]
158
+ Service:
159
+ section: '3.20'
160
+ groups: [observable]
161
+ fields: {ip-protocol: integer, ServiceName: ServiceName, Port: integer, Portlist: portlist, ProtoCode: integer, ProtoType: integer, ProtoField: integer, ApplicationHeaderField: 'Extension[]', EmailData: EmailData, Application: Software}
162
+ at_least_one: [ServiceName, Port, Portlist, ProtoCode, ProtoType, ProtoField, ApplicationHeaderField, EmailData, Application]
163
+ ServiceName:
164
+ section: '3.20.1'
165
+ groups: [described, linked]
166
+ fields: {IANAService: string}
167
+ at_least_one: [IANAService, URL, Description]
168
+ EmailData:
169
+ section: '3.21'
170
+ groups: [observable]
171
+ fields: {EmailTo: 'string[]', EmailFrom: string, EmailSubject: string, EmailX-Mailer: string, EmailHeaderField: 'Extension[]', EmailHeaders: string, EmailBody: string, EmailMessage: string, HashData: 'HashData[]', Signature: 'binary[]'}
172
+ RecordData:
173
+ section: '3.22'
174
+ groups: [restricted, observable, described, extensible, linked]
175
+ fields: {DateTime: datetime, Application: Software, RecordPattern: 'RecordPattern[]', RecordItem: 'Extension[]', FileData: 'FileData[]', WindowsRegistryKeysModified: 'WindowsRegistryKeysModified[]', CertificateData: 'CertificateData[]'}
176
+ at_least_one: [RecordItem, FileData, WindowsRegistryKeysModified, CertificateData]
177
+ RecordPattern:
178
+ section: '3.22.1'
179
+ fields: {value: 'string!', type: 'pattern_type!', ext-type: string, offset: integer, offsetunit: offset_unit, ext-offsetunit: string, instance: integer}
180
+ WindowsRegistryKeysModified:
181
+ section: '3.23'
182
+ groups: [observable]
183
+ fields: {Key: 'Key[]!'}
184
+ Key:
185
+ section: '3.23.1'
186
+ groups: [observable]
187
+ fields: {registryaction: registry_action, ext-registryaction: string, KeyName: 'string!', KeyValue: string}
188
+ CertificateData:
189
+ section: '3.24'
190
+ groups: [restricted, observable]
191
+ fields: {Certificate: 'Certificate[]!'}
192
+ Certificate:
193
+ section: '3.24.1'
194
+ groups: [observable, described]
195
+ fields: {X509Data: 'binary!'}
196
+ FileData:
197
+ section: '3.25'
198
+ groups: [restricted, observable]
199
+ fields: {File: 'File[]!'}
200
+ File:
201
+ section: '3.25.1'
202
+ groups: [observable, linked]
203
+ fields: {FileName: string, FileSize: integer, FileType: string, HashData: HashData, Signature: 'binary[]', AssociatedSoftware: Software, FileProperties: 'Extension[]'}
204
+ HashData:
205
+ section: '3.26'
206
+ fields: {scope: 'hash_scope!', ext-scope: string, HashTargetID: string, Hash: 'Digest[]', FuzzyHash: 'FuzzyHash[]'}
207
+ at_least_one: [Hash, FuzzyHash]
208
+ Digest:
209
+ section: '3.26.1'
210
+ fields: {DigestMethod: 'binary!', DigestValue: 'binary!', CanonicalizationMethod: binary, Application: Software}
211
+ FuzzyHash:
212
+ section: '3.26.2'
213
+ groups: [extensible]
214
+ fields: {FuzzyHashValue: 'Extension[]!', Application: Software}
215
+ Indicator:
216
+ section: '3.29'
217
+ groups: [restricted, described, extensible]
218
+ fields: {IndicatorID: 'IndicatorID!', AlternativeIndicatorID: 'AlternativeIndicatorID[]', StartTime: datetime, EndTime: datetime, Confidence: Confidence, Contact: 'Contact[]', Observable: Observable, uid-ref: id, IndicatorExpression: IndicatorExpression, IndicatorReference: IndicatorReference, NodeRole: 'NodeRole[]', AttackPhase: 'AttackPhase[]', Reference: 'Reference[]'}
219
+ exactly_one: [Observable, uid-ref, IndicatorExpression, IndicatorReference]
220
+ IndicatorID:
221
+ section: '3.29.1'
222
+ fields: {id: 'id!', name: 'string!', version: 'string!'}
223
+ AlternativeIndicatorID:
224
+ section: '3.29.2'
225
+ groups: [restricted]
226
+ fields: {IndicatorID: 'IndicatorID[]!'}
227
+ Observable:
228
+ section: '3.29.3'
229
+ groups: [restricted]
230
+ fields: {System: System, Address: Address, DomainData: DomainData, EmailData: EmailData, Service: Service, WindowsRegistryKeysModified: WindowsRegistryKeysModified, FileData: FileData, CertificateData: CertificateData, RegistryHandle: RegistryHandle, RecordData: RecordData, EventData: EventData, Incident: Incident, Expectation: Expectation, Reference: Reference, Assessment: Assessment, DetectionPattern: DetectionPattern, HistoryItem: HistoryItem, BulkObservable: BulkObservable, AdditionalData: 'Extension[]'}
231
+ exactly_one: [System, Address, DomainData, EmailData, Service, WindowsRegistryKeysModified, FileData, CertificateData, RegistryHandle, RecordData, EventData, Incident, Expectation, Reference, Assessment, DetectionPattern, HistoryItem, BulkObservable, AdditionalData]
232
+ BulkObservable:
233
+ section: '3.29.3.1'
234
+ groups: [extensible]
235
+ fields: {type: bulk_type, ext-type: string, BulkObservableFormat: BulkObservableFormat, BulkObservableList: 'string!'}
236
+ BulkObservableFormat:
237
+ section: '3.29.3.1.1'
238
+ groups: [extensible]
239
+ fields: {Hash: Digest}
240
+ at_least_one: [Hash, AdditionalData]
241
+ IndicatorExpression:
242
+ section: '3.29.4'
243
+ groups: [extensible]
244
+ fields: {operator: operator, ext-operator: string, IndicatorExpression: 'IndicatorExpression[]', Observable: 'Observable[]', uid-ref: 'id[]', IndicatorReference: 'IndicatorReference[]', Confidence: Confidence}
245
+ at_least_one: [IndicatorExpression, Observable, uid-ref, IndicatorReference]
246
+ IndicatorReference:
247
+ section: '3.29.7'
248
+ fields: {uid-ref: id, euid-ref: string, version: string}
249
+ exactly_one: [uid-ref, euid-ref]
250
+ AttackPhase:
251
+ section: '3.29.8'
252
+ groups: [described, extensible, linked]
253
+ fields: {AttackPhaseID: 'string[]'}
254
+ at_least_one: [AttackPhaseID, URL, Description, AdditionalData]
255
+ MLString:
256
+ section: '2.4'
257
+ fields: {value: 'string!', lang: language, translation-id: string}
258
+ Software:
259
+ section: '2.15'
260
+ groups: [linked, described]
261
+ fields: {SoftwareReference: SoftwareReference}
262
+ at_least_one: [SoftwareReference, URL, Description]
263
+ SoftwareReference:
264
+ section: '2.15.1'
265
+ fields: {value: string, spec-name: 'software_spec!', ext-spec-name: string, dtype: software_dtype, ext-dtype: string}
266
+ StructuredInformation:
267
+ section: 'RFC8727 2.2.5'
268
+ groups: [structured]
269
+ fields: {Platform: 'Platform[]', Scoring: 'Scoring[]'}
270
+ at_most_one: [RawData, Reference]
271
+ Platform:
272
+ section: 'RFC8727 2.2.5'
273
+ groups: [structured]
274
+ fields: {}
275
+ Scoring:
276
+ section: 'RFC8727 2.2.5'
277
+ groups: [structured]
278
+ fields: {}
279
+ Extension:
280
+ section: '2.16'
281
+ groups: [restricted, observable]
282
+ fields: {value: 'string!', name: string, dtype: 'dtype!', ext-dtype: string, meaning: string, formatid: string}
data/lib/iodef/cbor.rb ADDED
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "iodef"
4
+ require "cbor"
5
+ require_relative "cbor_guard"
6
+
7
+ module Iodef
8
+ module CBORCodec
9
+ module_function
10
+
11
+ Float32 = Struct.new(:value) do
12
+ def to_cbor(packer)
13
+ packer.buffer.write("\xfa".b + [value].pack("g"))
14
+ packer
15
+ end
16
+ end
17
+ private_constant :Float32
18
+
19
+ def encode(value, type = "Document")
20
+ return value.map { |item| encode(item, type.delete_suffix("[]")) } if type.end_with?("[]")
21
+
22
+ if value.is_a?(Hash)
23
+ fields = SCHEMA.fetch(type == "mlstring" ? "MLString" : type, {}).fetch("fields", {})
24
+ return value.to_h do |key, item|
25
+ descriptor = fields.fetch(key, "any").delete_suffix("!")
26
+ descriptor = content_type(value) if key == "value" && %w[Extension SoftwareReference].include?(type)
27
+ [MAPKEYS.fetch(key, key), encode(item, descriptor)]
28
+ end
29
+ end
30
+ return value.map { |item| encode(item, "any") } if value.is_a?(Array)
31
+ return CBOR::Tagged.new(22, value.unpack1("m0").b) if type == "binary"
32
+ return CBOR::Tagged.new(23, [value].pack("H*").b) if type == "hex"
33
+ return CBOR::Tagged.new(0, value) if type == "datetime"
34
+ return CBOR::Tagged.new(32, value) if type == "uri"
35
+
36
+ if %w[number positive].include?(type)
37
+ rounded = [value].pack("g").unpack1("g")
38
+ raise ConversionError, "IODEF CBOR real values must be exactly representable as float32" unless rounded.finite? && rounded == value
39
+
40
+ return Float32.new(rounded)
41
+ end
42
+ value
43
+ end
44
+
45
+ def decode(value, type = "Document", path = "")
46
+ if type.end_with?("[]") && value.is_a?(Array)
47
+ return value.each_with_index.map { |item, index| decode(item, type.delete_suffix("[]"), Iodef.pointer(path, index)) }
48
+ end
49
+ return decode_object(value, type, path) if value.is_a?(Hash)
50
+ return value.each_with_index.map { |item, index| decode(item, "any", Iodef.pointer(path, index)) } if value.is_a?(Array)
51
+
52
+ if value.is_a?(CBOR::Tagged) && [253, 254].include?(value.tag)
53
+ raise ConversionError, "bignum tag requires a byte string at #{path}" unless value.value.is_a?(String) && value.value.encoding == Encoding::BINARY
54
+
55
+ magnitude = value.value.empty? ? 0 : value.value.unpack1("H*").to_i(16)
56
+ value = value.tag == 254 ? -1 - magnitude : magnitude
57
+ end
58
+ case type
59
+ when "binary" then decode_tag(value, 22, path) { |bytes| [bytes].pack("m0") }
60
+ when "hex" then decode_tag(value, 23, path) { |bytes| bytes.unpack1("H*").upcase }
61
+ when "datetime" then decode_tag(value, 0, path) { |text| text }
62
+ when "uri" then decode_tag(value, 32, path) { |text| text }
63
+ else
64
+ if value.is_a?(CBOR::Tagged) || value.is_a?(CBOR::Simple) || (value.is_a?(String) && value.encoding == Encoding::BINARY)
65
+ raise ConversionError, "CBOR value has no unambiguous JSON representation at #{path}"
66
+ end
67
+
68
+ value
69
+ end
70
+ end
71
+
72
+ def decode_object(value, type, path)
73
+ fields = SCHEMA.fetch(type == "mlstring" ? "MLString" : type, {}).fetch("fields", {})
74
+ named = value.each_with_object({}) do |(key, item), result|
75
+ key = KEYNAMES.fetch(key) { raise ConversionError, "unassigned CBOR map key at #{path}" } if key.is_a?(Integer)
76
+ raise ConversionError, "CBOR map key must be a text string or assigned integer at #{path}" unless key.is_a?(String) && key.encoding != Encoding::BINARY
77
+ raise ParseError, "colliding CBOR map keys at #{path}" if result.key?(key)
78
+
79
+ result[key] = item
80
+ end
81
+ named.to_h do |key, item|
82
+ descriptor = fields.fetch(key, "any").delete_suffix("!")
83
+ descriptor = content_type(named) if key == "value" && %w[Extension SoftwareReference].include?(type)
84
+ [key, decode(item, descriptor, Iodef.pointer(path, key))]
85
+ end
86
+ end
87
+
88
+ def content_type(value)
89
+ case value["dtype"]
90
+ when "byte", "file" then "binary"
91
+ when "bytes", "frame", "packet", "ipv4-packet", "ipv6-packet" then "hex"
92
+ else "string"
93
+ end
94
+ end
95
+
96
+ def decode_tag(value, tag, path)
97
+ binary = [22, 23].include?(tag)
98
+ return yield(value) if value.is_a?(String) && (value.encoding == Encoding::BINARY) == binary
99
+
100
+ valid = value.is_a?(CBOR::Tagged) && value.tag == (tag == 32 ? 255 : tag) && value.value.is_a?(String)
101
+ valid &&= (value.value.encoding == Encoding::BINARY) == [22, 23].include?(tag)
102
+ raise ConversionError, "expected CBOR tag #{tag} at #{path}" unless valid
103
+
104
+ yield value.value
105
+ end
106
+ end
107
+
108
+ class << self
109
+ def parse_cbor(bytes, strict: true, validate: true, **limits)
110
+ options = self.limits(limits)
111
+ raise ParseError, "CBOR input must be a String" unless bytes.is_a?(String)
112
+ raise LimitError, "input exceeds max_input_size" if bytes.bytesize > options[:max_input_size]
113
+
114
+ guarded = CBORGuard.new(bytes, options).validate!
115
+ data = CBORCodec.decode(CBOR.decode(guarded))
116
+ Document.new(data, strict: strict && validate, **options)
117
+ rescue CBOR::UnpackError, CBOR::TypeError, EOFError, EncodingError
118
+ raise ParseError, "invalid CBOR input"
119
+ end
120
+ end
121
+
122
+ class Document
123
+ def to_cbor
124
+ validate!
125
+ bytes = CBOR.encode(CBORCodec.encode(to_h))
126
+ raise LimitError, "output exceeds max_input_size" if bytes.bytesize > limits[:max_input_size]
127
+
128
+ bytes
129
+ end
130
+ end
131
+ end