mihari 5.3.1 → 5.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -0
  3. data/frontend/package-lock.json +672 -652
  4. data/frontend/package.json +22 -22
  5. data/frontend/src/ace-config.ts +6 -0
  6. data/frontend/src/components/alert/Form.vue +2 -2
  7. data/frontend/src/components/rule/EditRule.vue +3 -2
  8. data/frontend/src/components/rule/Form.vue +2 -2
  9. data/frontend/src/components/rule/InputForm.vue +18 -59
  10. data/frontend/src/components/rule/YAML.vue +21 -28
  11. data/frontend/src/views/Artifact.vue +3 -8
  12. data/frontend/src/views/EditRule.vue +2 -7
  13. data/frontend/src/views/Rule.vue +3 -8
  14. data/lib/mihari/analyzers/base.rb +16 -3
  15. data/lib/mihari/analyzers/binaryedge.rb +2 -2
  16. data/lib/mihari/analyzers/censys.rb +2 -2
  17. data/lib/mihari/analyzers/hunterhow.rb +68 -0
  18. data/lib/mihari/analyzers/onyphe.rb +2 -2
  19. data/lib/mihari/analyzers/rule.rb +5 -7
  20. data/lib/mihari/analyzers/shodan.rb +2 -2
  21. data/lib/mihari/analyzers/urlscan.rb +2 -2
  22. data/lib/mihari/analyzers/virustotal_intelligence.rb +2 -2
  23. data/lib/mihari/analyzers/zoomeye.rb +4 -4
  24. data/lib/mihari/clients/hunterhow.rb +47 -0
  25. data/lib/mihari/commands/rule.rb +3 -3
  26. data/lib/mihari/commands/search.rb +3 -3
  27. data/lib/mihari/config.rb +46 -27
  28. data/lib/mihari/constants.rb +3 -3
  29. data/lib/mihari/emitters/base.rb +2 -2
  30. data/lib/mihari/emitters/misp.rb +3 -3
  31. data/lib/mihari/emitters/slack.rb +1 -1
  32. data/lib/mihari/emitters/the_hive.rb +1 -1
  33. data/lib/mihari/emitters/webhook.rb +1 -1
  34. data/lib/mihari/mixins/configurable.rb +5 -0
  35. data/lib/mihari/mixins/falsepositive.rb +1 -1
  36. data/lib/mihari/mixins/retriable.rb +0 -2
  37. data/lib/mihari/schemas/analyzer.rb +12 -2
  38. data/lib/mihari/schemas/rule.rb +1 -1
  39. data/lib/mihari/{structs → services}/rule.rb +16 -16
  40. data/lib/mihari/structs/hunterhow.rb +104 -0
  41. data/lib/mihari/version.rb +1 -1
  42. data/lib/mihari/web/endpoints/rules.rb +9 -8
  43. data/lib/mihari/web/public/assets/index-33165282.css +1 -0
  44. data/lib/mihari/web/public/assets/index-61dc587c.js +1738 -0
  45. data/lib/mihari/web/public/assets/mode-yaml-a21faa53.js +8 -0
  46. data/lib/mihari/web/public/index.html +2 -2
  47. data/lib/mihari.rb +6 -2
  48. data/mihari.gemspec +6 -5
  49. metadata +67 -20
  50. data/lib/mihari/web/public/assets/index-b17c40c6.css +0 -1
  51. data/lib/mihari/web/public/assets/index-f740e4f9.js +0 -799
@@ -95,7 +95,7 @@ module Mihari
95
95
  #
96
96
  def host_search
97
97
  responses = []
98
- (1..Float::INFINITY).each do |page|
98
+ (1..pagination_limit).each do |page|
99
99
  res = _host_search(query, page: page)
100
100
  break unless res
101
101
 
@@ -104,7 +104,7 @@ module Mihari
104
104
  break if total <= page * PAGE_SIZE
105
105
 
106
106
  # sleep #{interval} seconds to avoid the rate limitation (if it is set)
107
- sleep(interval) if interval
107
+ sleep_interval
108
108
  end
109
109
  convert_responses responses.compact
110
110
  end
@@ -128,7 +128,7 @@ module Mihari
128
128
  #
129
129
  def web_search
130
130
  responses = []
131
- (1..Float::INFINITY).each do |page|
131
+ (1..pagination_limit).each do |page|
132
132
  res = _web_search(query, page: page)
133
133
  break unless res
134
134
 
@@ -137,7 +137,7 @@ module Mihari
137
137
  break if total <= page * PAGE_SIZE
138
138
 
139
139
  # sleep #{interval} seconds to avoid the rate limitation (if it is set)
140
- sleep(interval) if interval
140
+ sleep_interval
141
141
  end
142
142
  convert_responses responses.compact
143
143
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+
5
+ module Mihari
6
+ module Clients
7
+ class HunterHow < Base
8
+ # @return [String]
9
+ attr_reader :api_key
10
+
11
+ #
12
+ # @param [String] base_url
13
+ # @param [String, nil] api_key
14
+ # @param [Hash] headers
15
+ #
16
+ def initialize(base_url = "https://api.hunter.how/", api_key:, headers: {})
17
+ raise(ArgumentError, "'api_key' argument is required") unless api_key
18
+
19
+ super(base_url, headers: headers)
20
+
21
+ @api_key = api_key
22
+ end
23
+
24
+ #
25
+ # @param [String] query String used to query our data
26
+ # @param [Integer] page Default 1, Maximum: 500
27
+ # @param [Integer] page_size Default 100, Maximum: 100
28
+ # @param [String] start_time
29
+ # @param [String] end_time
30
+ #
31
+ # @return [Structs::HunterHow::Response]
32
+ #
33
+ def search(query, start_time:, end_time:, page: 1, page_size: 10)
34
+ params = {
35
+ query: Base64.urlsafe_encode64(query),
36
+ page: page,
37
+ page_size: page_size,
38
+ start_time: start_time,
39
+ end_time: end_time,
40
+ "api-key": api_key
41
+ }.compact
42
+ res = get("/search", params: params)
43
+ Structs::HunterHow::Response.from_dynamic! JSON.parse(res.body.to_s)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -15,7 +15,7 @@ module Mihari
15
15
  # @param [String] path
16
16
  #
17
17
  def validate(path)
18
- rule = Structs::Rule.from_path_or_id(path)
18
+ rule = Services::Rule.from_path_or_id(path)
19
19
 
20
20
  begin
21
21
  rule.validate!
@@ -44,10 +44,10 @@ module Mihari
44
44
 
45
45
  no_commands do
46
46
  #
47
- # @return [Mihari::Structs::Rule]
47
+ # @return [Mihari::Services::Rule]
48
48
  #
49
49
  def rule_template
50
- Structs::Rule.from_path File.expand_path("../templates/rule.yml.erb", __dir__)
50
+ Services::Rule.from_path File.expand_path("../templates/rule.yml.erb", __dir__)
51
51
  end
52
52
 
53
53
  #
@@ -33,12 +33,12 @@ module Mihari
33
33
  end
34
34
 
35
35
  def update_or_create
36
- rule.model.save
36
+ rule.to_model.save
37
37
  end
38
38
 
39
39
  def run
40
40
  begin
41
- analyzer = rule.analyzer
41
+ analyzer = rule.to_analyzer
42
42
  rescue ConfigurationError => e
43
43
  # if there is a configuration error, output that error without the stack trace
44
44
  Mihari.logger.error e.to_s
@@ -69,7 +69,7 @@ module Mihari
69
69
  #
70
70
  def search(path_or_id)
71
71
  Mihari::Database.with_db_connection do
72
- rule = Structs::Rule.from_path_or_id path_or_id
72
+ rule = Services::Rule.from_path_or_id path_or_id
73
73
 
74
74
  begin
75
75
  rule.validate!
data/lib/mihari/config.rb CHANGED
@@ -1,85 +1,97 @@
1
1
  module Mihari
2
2
  class Config
3
3
  # @return [String, nil]
4
- attr_accessor :binaryedge_api_key
4
+ attr_reader :binaryedge_api_key
5
5
 
6
6
  # @return [String, nil]
7
- attr_accessor :censys_id
7
+ attr_reader :censys_id
8
8
 
9
9
  # @return [String, nil]
10
- attr_accessor :censys_secret
10
+ attr_reader :censys_secret
11
11
 
12
12
  # @return [String, nil]
13
- attr_accessor :circl_passive_password
13
+ attr_reader :circl_passive_password
14
14
 
15
15
  # @return [String, nil]
16
- attr_accessor :circl_passive_username
16
+ attr_reader :circl_passive_username
17
17
 
18
18
  # @return [URI]
19
- attr_accessor :database_url
19
+ attr_reader :database_url
20
20
 
21
21
  # @return [String, nil]
22
- attr_accessor :greynoise_api_key
22
+ attr_reader :greynoise_api_key
23
23
 
24
24
  # @return [String, nil]
25
- attr_accessor :ipinfo_api_key
25
+ attr_reader :hunterhow_api_key
26
26
 
27
27
  # @return [String, nil]
28
- attr_accessor :misp_url
28
+ attr_reader :ipinfo_api_key
29
29
 
30
30
  # @return [String, nil]
31
- attr_accessor :misp_api_key
31
+ attr_reader :misp_url
32
32
 
33
33
  # @return [String, nil]
34
- attr_accessor :onyphe_api_key
34
+ attr_reader :misp_api_key
35
35
 
36
36
  # @return [String, nil]
37
- attr_accessor :otx_api_key
37
+ attr_reader :onyphe_api_key
38
38
 
39
39
  # @return [String, nil]
40
- attr_accessor :passivetotal_api_key
40
+ attr_reader :otx_api_key
41
41
 
42
42
  # @return [String, nil]
43
- attr_accessor :passivetotal_username
43
+ attr_reader :passivetotal_api_key
44
44
 
45
45
  # @return [String, nil]
46
- attr_accessor :pulsedive_api_key
46
+ attr_reader :passivetotal_username
47
47
 
48
48
  # @return [String, nil]
49
- attr_accessor :securitytrails_api_key
49
+ attr_reader :pulsedive_api_key
50
50
 
51
51
  # @return [String, nil]
52
- attr_accessor :shodan_api_key
52
+ attr_reader :securitytrails_api_key
53
53
 
54
54
  # @return [String, nil]
55
- attr_accessor :slack_channel
55
+ attr_reader :shodan_api_key
56
56
 
57
57
  # @return [String, nil]
58
- attr_accessor :slack_webhook_url
58
+ attr_reader :slack_channel
59
59
 
60
60
  # @return [String, nil]
61
- attr_accessor :thehive_url
61
+ attr_reader :slack_webhook_url
62
62
 
63
63
  # @return [String, nil]
64
- attr_accessor :thehive_api_key
64
+ attr_reader :thehive_url
65
65
 
66
66
  # @return [String, nil]
67
- attr_accessor :thehive_api_version
67
+ attr_reader :thehive_api_key
68
68
 
69
69
  # @return [String, nil]
70
- attr_accessor :urlscan_api_key
70
+ attr_reader :thehive_api_version
71
71
 
72
72
  # @return [String, nil]
73
- attr_accessor :virustotal_api_key
73
+ attr_reader :urlscan_api_key
74
74
 
75
75
  # @return [String, nil]
76
- attr_accessor :zoomeye_api_key
76
+ attr_reader :virustotal_api_key
77
77
 
78
78
  # @return [String, nil]
79
- attr_accessor :sentry_dsn
79
+ attr_reader :zoomeye_api_key
80
80
 
81
81
  # @return [String, nil]
82
- attr_accessor :hide_config_values
82
+ attr_reader :sentry_dsn
83
+
84
+ # @return [Boolean]
85
+ attr_reader :hide_config_values
86
+
87
+ # @return [Integer]
88
+ attr_reader :retry_interval
89
+
90
+ # @return [Integer]
91
+ attr_reader :retry_times
92
+
93
+ # @return [Integer]
94
+ attr_reader :pagination_limit
83
95
 
84
96
  def initialize
85
97
  @binaryedge_api_key = ENV.fetch("BINARYEDGE_API_KEY", nil)
@@ -96,6 +108,8 @@ module Mihari
96
108
 
97
109
  @ipinfo_api_key = ENV.fetch("IPINFO_API_KEY", nil)
98
110
 
111
+ @hunterhow_api_key = ENV.fetch("HUNTERHOW_API_KEY", nil)
112
+
99
113
  @misp_url = ENV.fetch("MISP_URL", nil)
100
114
  @misp_api_key = ENV.fetch("MISP_API_KEY", nil)
101
115
 
@@ -128,6 +142,11 @@ module Mihari
128
142
  @sentry_dsn = ENV.fetch("SENTRY_DSN", nil)
129
143
 
130
144
  @hide_config_values = ENV.fetch("HIDE_CONFIG_VALUES", false)
145
+
146
+ @retry_times = ENV.fetch("RETRY_TIMES", 3).to_i
147
+ @retry_interval = ENV.fetch("RETRY_INTERVAL", 5).to_i
148
+
149
+ @pagination_limit = ENV.fetch("PAGINATION_LIMIT", 1000).to_i
131
150
  end
132
151
  end
133
152
  end
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mihari
4
+ # @return [Array<String>]
4
5
  DEFAULT_DATA_TYPES = %w[hash ip domain url mail].freeze
5
6
 
7
+ # @return [Array<Hash>]
6
8
  DEFAULT_EMITTERS = %w[database misp slack the_hive].map { |name| { emitter: name } }.freeze
7
9
 
10
+ # @return [Array<Hash>]
8
11
  DEFAULT_ENRICHERS = %w[whois ipinfo shodan google_public_dns].map { |name| { enricher: name } }.freeze
9
-
10
- DEFAULT_RETRY_TIMES = 3
11
- DEFAULT_RETRY_INTERVAL = 5
12
12
  end
@@ -9,12 +9,12 @@ module Mihari
9
9
  # @return [Array<Mihari::Artifact>]
10
10
  attr_reader :artifacts
11
11
 
12
- # @return [Mihari::Structs::Rule]
12
+ # @return [Mihari::Services::Rule]
13
13
  attr_reader :rule
14
14
 
15
15
  #
16
16
  # @param [Array<Mihari::Artifact>] artifacts
17
- # @param [Mihari::Structs::Rule] rule
17
+ # @param [Mihari::Services::Rule] rule
18
18
  # @param [Hash] **_options
19
19
  #
20
20
  def initialize(artifacts:, rule:, **_options)
@@ -12,12 +12,12 @@ module Mihari
12
12
  # @return [Array<Mihari::Artifact>]
13
13
  attr_reader :artifacts
14
14
 
15
- # @return [Mihari::Structs::Rule]
15
+ # @return [Mihari::Services::Rule]
16
16
  attr_reader :rule
17
17
 
18
18
  #
19
19
  # @param [Array<Mihari::Artifact>] artifacts
20
- # @param [Mihari::Structs::Rule] rule
20
+ # @param [Mihari::Services::Rule] rule
21
21
  # @param [Hash] **options
22
22
  #
23
23
  def initialize(artifacts:, rule:, **options)
@@ -47,7 +47,7 @@ module Mihari
47
47
  # Create a MISP event
48
48
  #
49
49
  # @param [Arra<Mihari::Artifact>] artifacts
50
- # @param [Mihari::Structs::Rule] rule
50
+ # @param [Mihari::Services::Rule] rule
51
51
  #
52
52
  # @return [::MISP::Event]
53
53
  #
@@ -133,7 +133,7 @@ module Mihari
133
133
 
134
134
  #
135
135
  # @param [Array<Mihari::Artifact>] artifacts
136
- # @param [Mihari::Structs::Rule] rule
136
+ # @param [Mihari::Services::Rule] rule
137
137
  # @param [Hash] **_options
138
138
  #
139
139
  def initialize(artifacts:, rule:, **options)
@@ -14,7 +14,7 @@ module Mihari
14
14
 
15
15
  #
16
16
  # @param [Array<Mihari::Artifact>] artifacts
17
- # @param [Mihari::Structs::Rule] rule
17
+ # @param [Mihari::Services::Rule] rule
18
18
  # @param [Hash] **options
19
19
  #
20
20
  def initialize(artifacts:, rule:, **options)
@@ -57,7 +57,7 @@ module Mihari
57
57
 
58
58
  #
59
59
  # @param [Array<Mihari::Artifact>] artifacts
60
- # @param [Mihari::Structs::Rule] rule
60
+ # @param [Mihari::Services::Rule] rule
61
61
  # @param [Hash] **options
62
62
  #
63
63
  def initialize(artifacts:, rule:, **options)
@@ -49,6 +49,11 @@ module Mihari
49
49
 
50
50
  private
51
51
 
52
+ #
53
+ # Check whether API key is set or not
54
+ #
55
+ # @return [Boolean]
56
+ #
52
57
  def api_key?
53
58
  value = method(:api_key).call
54
59
  !value.nil?
@@ -22,7 +22,7 @@ module Mihari
22
22
  memoize :normalize_falsepositive
23
23
 
24
24
  #
25
- # Check whetehr a value is valid format as a disallowed data value
25
+ # Check whether a value is valid format as a disallowed data value
26
26
  #
27
27
  # @param [String] value Data value
28
28
  #
@@ -21,8 +21,6 @@ module Mihari
21
21
  # @param [Integer] interval
22
22
  # @param [Array<StandardError>] on
23
23
  #
24
- # @return [nil]
25
- #
26
24
  def retry_on_error(times: 3, interval: 5, on: DEFAULT_ON)
27
25
  try = 0
28
26
  begin
@@ -4,8 +4,9 @@ module Mihari
4
4
  module Schemas
5
5
  AnalyzerOptions = Dry::Schema.Params do
6
6
  optional(:interval).value(:integer)
7
- optional(:retry_times).value(:integer).default(DEFAULT_RETRY_TIMES)
8
- optional(:retry_interval).value(:integer).default(DEFAULT_RETRY_INTERVAL)
7
+ optional(:pagination_limit).value(:integer).default(Mihari.config.pagination_limit)
8
+ optional(:retry_times).value(:integer).default(Mihari.config.retry_times)
9
+ optional(:retry_interval).value(:integer).default(Mihari.config.retry_interval)
9
10
  end
10
11
 
11
12
  AnalyzerWithoutAPIKey = Dry::Schema.Params do
@@ -75,6 +76,15 @@ module Mihari
75
76
  optional(:options).hash(AnalyzerOptions)
76
77
  end
77
78
 
79
+ HunterHow = Dry::Schema.Params do
80
+ required(:analyzer).value(Types::String.enum("hunterhow"))
81
+ required(:query).value(:string)
82
+ required(:start_time).value(:date)
83
+ required(:end_time).value(:date)
84
+ optional(:api_key).value(:string)
85
+ optional(:options).hash(AnalyzerOptions)
86
+ end
87
+
78
88
  Feed = Dry::Schema.Params do
79
89
  required(:analyzer).value(Types::String.enum("feed"))
80
90
  required(:query).value(:string)
@@ -22,7 +22,7 @@ module Mihari
22
22
  optional(:updated_on).value(:date)
23
23
 
24
24
  required(:queries).value(:array).each do
25
- AnalyzerWithoutAPIKey | AnalyzerWithAPIKey | Censys | CIRCL | PassiveTotal | ZoomEye | Crtsh | Feed
25
+ AnalyzerWithoutAPIKey | AnalyzerWithAPIKey | Censys | CIRCL | PassiveTotal | ZoomEye | Crtsh | Feed | HunterHow
26
26
  end
27
27
 
28
28
  optional(:emitters).value(:array).each { Database | MISP | TheHive | Slack | Webhook }.default(DEFAULT_EMITTERS)
@@ -8,7 +8,7 @@ require "securerandom"
8
8
  require "yaml"
9
9
 
10
10
  module Mihari
11
- module Structs
11
+ module Services
12
12
  class Rule
13
13
  include Mixins::FalsePositive
14
14
 
@@ -49,16 +49,16 @@ module Mihari
49
49
  end
50
50
 
51
51
  def validate!
52
- raise RuleValidationError if errors?
53
- rescue RuleValidationError => e
52
+ return unless errors?
53
+
54
54
  Mihari.logger.error "Failed to parse the input as a rule:"
55
55
  Mihari.logger.error JSON.pretty_generate(errors.to_h)
56
56
 
57
- raise e
57
+ raise RuleValidationError, errors
58
58
  end
59
59
 
60
60
  def [](key)
61
- data[key.to_sym]
61
+ data key.to_sym
62
62
  end
63
63
 
64
64
  #
@@ -141,7 +141,7 @@ module Mihari
141
141
  #
142
142
  # @return [Mihari::Rule]
143
143
  #
144
- def model
144
+ def to_model
145
145
  rule = Mihari::Rule.find(id)
146
146
 
147
147
  rule.title = title
@@ -161,8 +161,8 @@ module Mihari
161
161
  #
162
162
  # @return [Mihari::Analyzers::Rule]
163
163
  #
164
- def analyzer
165
- Mihari::Analyzers::Rule.new(self)
164
+ def to_analyzer
165
+ Mihari::Analyzers::Rule.new self
166
166
  end
167
167
 
168
168
  class << self
@@ -171,10 +171,10 @@ module Mihari
171
171
  #
172
172
  # @param [String] yaml
173
173
  #
174
- # @return [Mihari::Structs::Rule]
174
+ # @return [Mihari::Services::Rule]
175
175
  #
176
176
  def from_yaml(yaml)
177
- Structs::Rule.new YAML.safe_load(ERB.new(yaml).result, permitted_classes: [Date, Symbol])
177
+ Services::Rule.new YAML.safe_load(ERB.new(yaml).result, permitted_classes: [Date, Symbol])
178
178
  rescue Psych::SyntaxError => e
179
179
  raise YAMLSyntaxError, e.message
180
180
  end
@@ -182,10 +182,10 @@ module Mihari
182
182
  #
183
183
  # @param [Mihari::Rule] model
184
184
  #
185
- # @return [Mihari::Structs::Rule]
185
+ # @return [Mihari::Services::Rule]
186
186
  #
187
187
  def from_model(model)
188
- Structs::Rule.new(model.data)
188
+ Services::Rule.new model.data
189
189
  end
190
190
 
191
191
  #
@@ -193,7 +193,7 @@ module Mihari
193
193
  #
194
194
  # @param [String] path
195
195
  #
196
- # @return [Mihari::Structs::Rule, nil]
196
+ # @return [Mihari::Services::Rule, nil]
197
197
  #
198
198
  def from_path(path)
199
199
  return nil unless Pathname(path).exist?
@@ -206,18 +206,18 @@ module Mihari
206
206
  #
207
207
  # @param [String] id
208
208
  #
209
- # @return [Mihari::Structs::Rule, nil]
209
+ # @return [Mihari::Services::Rule, nil]
210
210
  #
211
211
  def from_id(id)
212
212
  return nil unless Mihari::Rule.exists?(id)
213
213
 
214
- Structs::Rule.from_model Mihari::Rule.find(id)
214
+ Services::Rule.from_model Mihari::Rule.find(id)
215
215
  end
216
216
 
217
217
  #
218
218
  # @param [String] path_or_id Path to YAML file or YAML string or ID of a rule in the database
219
219
  #
220
- # @return [Mihari::Structs::Rule]
220
+ # @return [Mihari::Services::Rule]
221
221
  #
222
222
  def from_path_or_id(path_or_id)
223
223
  rule = from_path(path_or_id)
@@ -0,0 +1,104 @@
1
+ module Mihari
2
+ module Structs
3
+ module HunterHow
4
+ class ListItem < Dry::Struct
5
+ attribute :domain, Types::String
6
+ attribute :ip, Types::String
7
+ attribute :port, Types::Integer
8
+
9
+ #
10
+ # @return [String]
11
+ #
12
+ def ip
13
+ attributes[:ip]
14
+ end
15
+
16
+ #
17
+ # @return [Mihari::Artifact]
18
+ #
19
+ def artifact
20
+ Artifact.new(data: ip)
21
+ end
22
+
23
+ class << self
24
+ #
25
+ # @param [Hash] d
26
+ #
27
+ # @return [ListItem]
28
+ #
29
+ def from_dynamic!(d)
30
+ d = Types::Hash[d]
31
+ new(
32
+ domain: d.fetch("domain"),
33
+ ip: d.fetch("ip"),
34
+ port: d.fetch("port")
35
+ )
36
+ end
37
+ end
38
+ end
39
+
40
+ class DataClass < Dry::Struct
41
+ attribute :list, Types.Array(ListItem)
42
+ attribute :total, Types::Integer
43
+
44
+ #
45
+ # @return [Array<ListItem>]
46
+ #
47
+ def list
48
+ attributes[:list]
49
+ end
50
+
51
+ #
52
+ # @return [Array<Mihari::Artifact>]
53
+ #
54
+ def artifacts
55
+ list.map(&:artifact)
56
+ end
57
+
58
+ class << self
59
+ #
60
+ # @param [Hash] d
61
+ #
62
+ # @return [DataClass]
63
+ #
64
+ def from_dynamic!(d)
65
+ d = Types::Hash[d]
66
+ new(
67
+ list: d.fetch("list").map { |x| ListItem.from_dynamic!(x) },
68
+ total: d.fetch("total")
69
+ )
70
+ end
71
+ end
72
+ end
73
+
74
+ class Response < Dry::Struct
75
+ attribute :code, Types::Integer
76
+ attribute :data, DataClass
77
+ attribute :message, Types::String
78
+
79
+ #
80
+ # @return [DataClass]
81
+ #
82
+ def data
83
+ attributes[:data]
84
+ end
85
+
86
+ class << self
87
+ #
88
+ # @param [Hash] d
89
+ #
90
+ # @return [Response]
91
+ #
92
+ def from_dynamic!(d)
93
+ d = Types::Hash[d]
94
+ new(
95
+ code: d.fetch("code"),
96
+ data: DataClass.from_dynamic!(d.fetch("data")),
97
+ message: d.fetch("message")
98
+ )
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mihari
4
- VERSION = "5.3.1"
4
+ VERSION = "5.4.0"
5
5
  end