mihari 3.9.2 → 3.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/docker/Dockerfile +1 -1
  4. data/lib/mihari/analyzers/base.rb +1 -1
  5. data/lib/mihari/analyzers/binaryedge.rb +5 -0
  6. data/lib/mihari/analyzers/censys.rb +5 -0
  7. data/lib/mihari/analyzers/feed.rb +39 -0
  8. data/lib/mihari/analyzers/greynoise.rb +65 -0
  9. data/lib/mihari/analyzers/onyphe.rb +5 -0
  10. data/lib/mihari/analyzers/rule.rb +8 -0
  11. data/lib/mihari/analyzers/shodan.rb +16 -5
  12. data/lib/mihari/analyzers/urlscan.rb +37 -13
  13. data/lib/mihari/analyzers/virustotal_intelligence.rb +5 -0
  14. data/lib/mihari/analyzers/zoomeye.rb +8 -0
  15. data/lib/mihari/cli/analyzer.rb +5 -0
  16. data/lib/mihari/commands/feed.rb +26 -0
  17. data/lib/mihari/commands/greynoise.rb +21 -0
  18. data/lib/mihari/commands/urlscan.rb +1 -2
  19. data/lib/mihari/database.rb +4 -4
  20. data/lib/mihari/enrichers/ipinfo.rb +2 -0
  21. data/lib/mihari/errors.rb +4 -0
  22. data/lib/mihari/feed/parser.rb +34 -0
  23. data/lib/mihari/feed/reader.rb +113 -0
  24. data/lib/mihari/mixins/autonomous_system.rb +2 -0
  25. data/lib/mihari/mixins/configuration.rb +2 -2
  26. data/lib/mihari/mixins/disallowed_data_value.rb +2 -0
  27. data/lib/mihari/mixins/rule.rb +2 -0
  28. data/lib/mihari/models/alert.rb +4 -5
  29. data/lib/mihari/models/artifact.rb +2 -3
  30. data/lib/mihari/models/dns.rb +2 -2
  31. data/lib/mihari/schemas/configuration.rb +3 -2
  32. data/lib/mihari/schemas/rule.rb +21 -2
  33. data/lib/mihari/status.rb +2 -2
  34. data/lib/mihari/structs/alert.rb +3 -1
  35. data/lib/mihari/structs/censys.rb +2 -0
  36. data/lib/mihari/structs/greynoise.rb +57 -0
  37. data/lib/mihari/structs/ipinfo.rb +2 -0
  38. data/lib/mihari/structs/onyphe.rb +2 -0
  39. data/lib/mihari/structs/shodan.rb +8 -6
  40. data/lib/mihari/structs/urlscan.rb +53 -0
  41. data/lib/mihari/structs/virustotal_intelligence.rb +2 -0
  42. data/lib/mihari/types.rb +10 -0
  43. data/lib/mihari/version.rb +1 -1
  44. data/lib/mihari/web/api.rb +2 -0
  45. data/lib/mihari/web/entities/artifact.rb +2 -2
  46. data/lib/mihari/web/entities/whois.rb +1 -1
  47. data/lib/mihari/web/public/index.html +1 -1
  48. data/lib/mihari/web/public/redoc-static.html +181 -183
  49. data/lib/mihari/web/public/static/js/app.0a0cc502.js +21 -0
  50. data/lib/mihari/web/public/static/js/app.0a0cc502.js.map +1 -0
  51. data/lib/mihari/web/public/static/js/app.5dc97aae.js +21 -0
  52. data/lib/mihari/web/public/static/js/app.5dc97aae.js.map +1 -0
  53. data/lib/mihari/web/public/static/js/app.f2b8890f.js +21 -0
  54. data/lib/mihari/web/public/static/js/app.f2b8890f.js.map +1 -0
  55. data/lib/mihari/web/public/static/js/app.fbc19869.js +21 -0
  56. data/lib/mihari/web/public/static/js/app.fbc19869.js.map +1 -0
  57. data/lib/mihari.rb +7 -2
  58. data/mihari.gemspec +22 -21
  59. data/sig/lib/mihari/analyzers/binaryedge.rbs +2 -0
  60. data/sig/lib/mihari/analyzers/censys.rbs +2 -0
  61. data/sig/lib/mihari/analyzers/feed.rbs +23 -0
  62. data/sig/lib/mihari/analyzers/onyphe.rbs +2 -0
  63. data/sig/lib/mihari/analyzers/shodan.rbs +2 -0
  64. data/sig/lib/mihari/analyzers/urlscan.rbs +5 -2
  65. data/sig/lib/mihari/analyzers/virustotal_intelligence.rbs +2 -0
  66. data/sig/lib/mihari/analyzers/zoomeye.rbs +2 -0
  67. data/sig/lib/mihari/cli/analyzer.rbs +4 -0
  68. data/sig/lib/mihari/commands/feed.rbs +7 -0
  69. data/sig/lib/mihari/feed/parser.rbs +11 -0
  70. data/sig/lib/mihari/feed/reader.rbs +56 -0
  71. data/sig/lib/mihari/structs/alert.rbs +1 -1
  72. data/sig/lib/mihari/structs/greynoise.rbs +30 -0
  73. data/sig/lib/mihari/structs/shodan.rbs +3 -3
  74. data/sig/lib/mihari/structs/urlscan.rbs +28 -0
  75. data/sig/lib/mihari/types.rbs +4 -0
  76. metadata +120 -84
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+ require "json"
5
+ require "net/https"
6
+ require "uri"
7
+
8
+ module Mihari
9
+ module Feed
10
+ class Reader
11
+ attr_reader :uri, :http_request_headers, :http_request_method, :http_request_payload_type, :http_request_payload
12
+
13
+ def initialize(url, http_request_headers: {}, http_request_method: "GET", http_request_payload_type: nil, http_request_payload: {})
14
+ @uri = URI(url)
15
+ @http_request_headers = http_request_headers
16
+ @http_request_method = http_request_method
17
+ @http_request_payload_type = http_request_payload_type
18
+ @http_request_payload = http_request_payload
19
+ end
20
+
21
+ def read
22
+ return get if http_request_method == "GET"
23
+
24
+ post
25
+ end
26
+
27
+ def get
28
+ uri.query = URI.encode_www_form(http_request_payload)
29
+ get = Net::HTTP::Get.new(uri)
30
+
31
+ request(get)
32
+ end
33
+
34
+ def post
35
+ post = Net::HTTP::Post.new(uri)
36
+
37
+ case http_request_payload_type
38
+ when "application/json"
39
+ post.body = JSON.generate(http_request_payload)
40
+ when "application/x-www-form-urlencoded"
41
+ post.set_form_data(http_request_payload)
42
+ end
43
+
44
+ request(post)
45
+ end
46
+
47
+ #
48
+ # Convert text as JSON
49
+ #
50
+ # @param [String] text
51
+ #
52
+ # @return [Array<Hash>]
53
+ #
54
+ def convert_as_json(text)
55
+ data = JSON.parse(text, symbolize_names: true)
56
+ return data if data.is_a?(Array)
57
+
58
+ [data]
59
+ end
60
+
61
+ #
62
+ # Convert text as CSV
63
+ #
64
+ # @param [String] text
65
+ #
66
+ # @return [Array<Hash>]
67
+ #
68
+ def convert_as_csv(text)
69
+ text_without_comments = text.lines.reject { |line| line.start_with? "#" }.join("\n")
70
+
71
+ CSV.new(text_without_comments).to_a.reject(&:empty?)
72
+ end
73
+
74
+ def https_options
75
+ return { use_ssl: true } if uri.scheme == "https"
76
+
77
+ {}
78
+ end
79
+
80
+ #
81
+ # Make a HTTP request
82
+ #
83
+ # @param [Net::HTTPRequest] req
84
+ #
85
+ # @return [Array<Hash>]
86
+ #
87
+ def request(req)
88
+ Net::HTTP.start(uri.host, uri.port, https_options) do |http|
89
+ # set headers
90
+ http_request_headers.each do |k, v|
91
+ req[k] = v
92
+ end
93
+
94
+ response = http.request(req)
95
+
96
+ code = response.code.to_i
97
+ raise HttpError, "Unsupported response code returned: #{code}" if code != 200
98
+
99
+ body = response.body
100
+
101
+ content_type = response["Content-Type"].to_s
102
+ data = if content_type.include?("application/json")
103
+ convert_as_json(body)
104
+ else
105
+ convert_as_csv(body)
106
+ end
107
+
108
+ data
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Mihari
2
4
  module Mixins
3
5
  module AutonomousSystem
@@ -51,9 +51,9 @@ module Mihari
51
51
  # @return [String] A template for config
52
52
  #
53
53
  def config_template
54
- config = Mihari.config.values.keys.map do |key|
54
+ config = Mihari.config.values.keys.to_h do |key|
55
55
  [key.to_s, nil]
56
- end.to_h
56
+ end
57
57
 
58
58
  YAML.dump(config)
59
59
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "mem"
2
4
 
3
5
  module Mihari
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "date"
2
4
  require "pathname"
3
5
  require "yaml"
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_record"
4
- require "active_record/filter"
5
4
 
6
5
  module Mihari
7
6
  class Alert < ActiveRecord::Base
@@ -55,7 +54,7 @@ module Mihari
55
54
  artifact = artifact.where(dns_records: { value: filter.dns_record }) if filter.dns_record
56
55
  artifact = artifact.where(reverse_dns_names: { name: filter.reverse_dns_name }) if filter.reverse_dns_name
57
56
  # get artifact ids if there is any valid filter for artifact
58
- if filter.has_valid_artifact_filters
57
+ if filter.valid_artifact_filters?
59
58
  artifact_ids = artifact.pluck(:id)
60
59
  # set invalid ID if nothing is matched with the filters
61
60
  artifact_ids = [-1] if artifact_ids.empty?
@@ -70,10 +69,10 @@ module Mihari
70
69
  relation = relation.where(source: filter.source) if filter.source
71
70
  relation = relation.where(title: filter.title) if filter.title
72
71
 
73
- relation = relation.filter(description: { like: "%#{filter.description}%" }) if filter.description
72
+ relation = relation.where("description LIKE ?", "%#{filter.description}%") if filter.description
74
73
 
75
- relation = relation.filter(created_at: { gte: filter.from_at }) if filter.from_at
76
- relation = relation.filter(created_at: { lte: filter.to_at }) if filter.to_at
74
+ relation = relation.where("alerts.created_at >= ?", filter.from_at) if filter.from_at
75
+ relation = relation.where("alerts.created_at <= ?", filter.to_at) if filter.to_at
77
76
 
78
77
  relation
79
78
  end
@@ -1,10 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_record"
4
- require "active_record/filter"
5
4
  require "active_support/core_ext/integer/time"
6
5
  require "active_support/core_ext/numeric/time"
7
- require "uri"
6
+ require "addressable/uri"
8
7
 
9
8
  class ArtifactValidator < ActiveModel::Validator
10
9
  def validate(record)
@@ -119,7 +118,7 @@ module Mihari
119
118
  def normalize_as_domain(url_or_domain)
120
119
  return url_or_domain if data_type == "domain"
121
120
 
122
- URI.parse(url_or_domain).host
121
+ Addressable::URI.parse(url_or_domain).host
123
122
  end
124
123
 
125
124
  def can_enrich_whois?
@@ -37,7 +37,7 @@ module Mihari
37
37
  resources = Resolv::DNS.new.getresources(domain, resource_type)
38
38
  resource_name = resource_type.to_s.split("::").last
39
39
 
40
- resources.map do |resource|
40
+ resources.filter_map do |resource|
41
41
  # A, AAAA
42
42
  if resource.respond_to?(:address)
43
43
  new(resource: resource_name, value: resource.address.to_s)
@@ -48,7 +48,7 @@ module Mihari
48
48
  elsif resource.respond_to?(:data)
49
49
  new(resource: resource_name, value: resource.data.to_s)
50
50
  end
51
- end.compact
51
+ end
52
52
  end
53
53
  end
54
54
  end
@@ -13,6 +13,8 @@ module Mihari
13
13
  optional(:censys_secret).value(:string)
14
14
  optional(:circl_passive_password).value(:string)
15
15
  optional(:circl_passive_username).value(:string)
16
+ optional(:database).value(:string)
17
+ optional(:greynoise_api_key).value(:string)
16
18
  optional(:ipinfo_api_key).value(:string)
17
19
  optional(:misp_api_endpoint).value(:string)
18
20
  optional(:misp_api_key).value(:string)
@@ -30,10 +32,9 @@ module Mihari
30
32
  optional(:thehive_api_key).value(:string)
31
33
  optional(:urlscan_api_key).value(:string)
32
34
  optional(:virustotal_api_key).value(:string)
33
- optional(:zoomeye_api_key).value(:string)
34
35
  optional(:webhook_url).value(:string)
35
36
  optional(:webhook_use_json_body).value(:bool)
36
- optional(:database).value(:string)
37
+ optional(:zoomeye_api_key).value(:string)
37
38
  end
38
39
 
39
40
  class ConfigurationContract < Dry::Validation::Contract
@@ -7,33 +7,52 @@ require "mihari/schemas/macros"
7
7
 
8
8
  module Mihari
9
9
  module Schemas
10
+ AnalyzerOptions = Dry::Schema.Params do
11
+ optional(:interval).value(:integer)
12
+ end
13
+
10
14
  Analyzer = Dry::Schema.Params do
11
15
  required(:analyzer).value(Types::AnalyzerTypes)
12
16
  required(:query).value(:string)
17
+ optional(:options).hash(AnalyzerOptions)
13
18
  end
14
19
 
15
20
  Spyse = Dry::Schema.Params do
16
21
  required(:analyzer).value(Types::String.enum("spyse"))
17
22
  required(:query).value(:string)
18
23
  required(:type).value(Types::String.enum("ip", "domain"))
24
+ optional(:options).hash(AnalyzerOptions)
19
25
  end
20
26
 
21
27
  ZoomEye = Dry::Schema.Params do
22
28
  required(:analyzer).value(Types::String.enum("zoomeye"))
23
29
  required(:query).value(:string)
24
30
  required(:type).value(Types::String.enum("host", "web"))
31
+ optional(:options).hash(AnalyzerOptions)
25
32
  end
26
33
 
27
34
  Crtsh = Dry::Schema.Params do
28
35
  required(:analyzer).value(Types::String.enum("crtsh"))
29
36
  required(:query).value(:string)
30
37
  optional(:exclude_expired).value(:bool).default(true)
38
+ optional(:options).hash(AnalyzerOptions)
31
39
  end
32
40
 
33
41
  Urlscan = Dry::Schema.Params do
34
42
  required(:analyzer).value(Types::String.enum("urlscan"))
35
43
  required(:query).value(:string)
36
- optional(:use_similarity).value(:bool).default(true)
44
+ optional(:options).hash(AnalyzerOptions)
45
+ end
46
+
47
+ Feed = Dry::Schema.Params do
48
+ required(:analyzer).value(Types::String.enum("feed"))
49
+ required(:query).value(:string)
50
+ required(:http_request_method).value(Types::FeedHttpRequestMethods).default("GET")
51
+ required(:http_request_headers).value(:hash).default({})
52
+ required(:http_request_payload).value(:hash).default({})
53
+ required(:selector).value(:string)
54
+ optional(:http_request_payload_type).value(Types::FeedHttpRequestPayloadTypes)
55
+ optional(:options).hash(AnalyzerOptions)
37
56
  end
38
57
 
39
58
  Rule = Dry::Schema.Params do
@@ -47,7 +66,7 @@ module Mihari
47
66
  optional(:created_on).value(:date)
48
67
  optional(:updated_on).value(:date)
49
68
 
50
- required(:queries).value(:array).each { Analyzer | Spyse | ZoomEye | Urlscan | Crtsh }
69
+ required(:queries).value(:array).each { Analyzer | Spyse | ZoomEye | Urlscan | Crtsh | Feed }
51
70
 
52
71
  optional(:allowed_data_types).value(array[Types::DataTypes]).default(ALLOWED_DATA_TYPES)
53
72
  optional(:disallowed_data_values).value(array[:string]).default([])
data/lib/mihari/status.rb CHANGED
@@ -18,11 +18,11 @@ module Mihari
18
18
  # @return [Array<Hash>]
19
19
  #
20
20
  def statuses
21
- (Mihari.analyzers + Mihari.emitters + Mihari.enrichers).map do |klass|
21
+ (Mihari.analyzers + Mihari.emitters + Mihari.enrichers).to_h do |klass|
22
22
  name = klass.to_s.split("::").last.to_s
23
23
 
24
24
  [name, build_status(klass)]
25
- end.to_h.compact
25
+ end.compact
26
26
  end
27
27
 
28
28
  #
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "json"
2
4
  require "dry/struct"
3
5
 
@@ -16,7 +18,7 @@ module Mihari
16
18
  attribute? :dns_record, Types::String.optional
17
19
  attribute? :reverse_dns_name, Types::String.optional
18
20
 
19
- def has_valid_artifact_filters
21
+ def valid_artifact_filters?
20
22
  !(artifact_data || asn || dns_record || reverse_dns_name).nil?
21
23
  end
22
24
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "json"
2
4
  require "dry/struct"
3
5
 
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "dry/struct"
5
+
6
+ module Mihari
7
+ module Structs
8
+ module GreyNoise
9
+ class Metadata < Dry::Struct
10
+ attribute :country, Types::String
11
+ attribute :country_code, Types::String
12
+ attribute :asn, Types::String
13
+
14
+ def self.from_dynamic!(d)
15
+ d = Types::Hash[d]
16
+ new(
17
+ country: d.fetch("country"),
18
+ country_code: d.fetch("country_code"),
19
+ asn: d.fetch("asn")
20
+ )
21
+ end
22
+ end
23
+
24
+ class Datum < Dry::Struct
25
+ attribute :ip, Types::String
26
+ attribute :metadata, Metadata
27
+
28
+ def self.from_dynamic!(d)
29
+ d = Types::Hash[d]
30
+ new(
31
+ ip: d.fetch("ip"),
32
+ metadata: Metadata.from_dynamic!(d.fetch("metadata"))
33
+ )
34
+ end
35
+ end
36
+
37
+ class Response < Dry::Struct
38
+ attribute :complete, Types::Bool
39
+ attribute :count, Types::Int
40
+ attribute :data, Types.Array(Datum)
41
+ attribute :message, Types::String
42
+ attribute :query, Types::String
43
+
44
+ def self.from_dynamic!(d)
45
+ d = Types::Hash[d]
46
+ new(
47
+ complete: d.fetch("complete"),
48
+ count: d.fetch("count"),
49
+ data: d.fetch("data").map { |x| Datum.from_dynamic!(x) },
50
+ message: d.fetch("message"),
51
+ query: d.fetch("query")
52
+ )
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "json"
2
4
  require "dry/struct"
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "json"
2
4
  require "dry/struct"
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "json"
2
4
  require "dry/struct"
3
5
 
@@ -5,20 +7,20 @@ module Mihari
5
7
  module Structs
6
8
  module Shodan
7
9
  class Location < Dry::Struct
8
- attribute :country_code, Types::String
9
- attribute :country_name, Types::String
10
+ attribute :country_code, Types::String.optional
11
+ attribute :country_name, Types::String.optional
10
12
 
11
13
  def self.from_dynamic!(d)
12
14
  d = Types::Hash[d]
13
15
  new(
14
- country_code: d.fetch("country_code"),
15
- country_name: d.fetch("country_name")
16
+ country_code: d["country_code"],
17
+ country_name: d["country_name"]
16
18
  )
17
19
  end
18
20
  end
19
21
 
20
22
  class Match < Dry::Struct
21
- attribute :asn, Types::String
23
+ attribute :asn, Types::String.optional
22
24
  attribute :hostnames, Types.Array(Types::String)
23
25
  attribute :location, Location
24
26
  attribute :domains, Types.Array(Types::String)
@@ -27,7 +29,7 @@ module Mihari
27
29
  def self.from_dynamic!(d)
28
30
  d = Types::Hash[d]
29
31
  new(
30
- asn: d.fetch("asn"),
32
+ asn: d["asn"],
31
33
  hostnames: d.fetch("hostnames"),
32
34
  location: Location.from_dynamic!(d.fetch("location")),
33
35
  domains: d.fetch("domains"),
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "dry/struct"
5
+
6
+ module Mihari
7
+ module Structs
8
+ module Urlscan
9
+ class Page < Dry::Struct
10
+ attribute :domain, Types::String.optional
11
+ attribute :ip, Types::String.optional
12
+ attribute :url, Types::String
13
+
14
+ def self.from_dynamic!(d)
15
+ d = Types::Hash[d]
16
+ new(
17
+ domain: d["domain"],
18
+ ip: d["ip"],
19
+ url: d.fetch("url")
20
+ )
21
+ end
22
+ end
23
+
24
+ class Result < Dry::Struct
25
+ attribute :page, Page
26
+ attribute :id, Types::String
27
+ attribute :sort, Types.Array(Types::String | Types::Integer)
28
+
29
+ def self.from_dynamic!(d)
30
+ d = Types::Hash[d]
31
+ new(
32
+ page: Page.from_dynamic!(d.fetch("page")),
33
+ id: d.fetch("_id"),
34
+ sort: d.fetch("sort")
35
+ )
36
+ end
37
+ end
38
+
39
+ class Response < Dry::Struct
40
+ attribute :results, Types.Array(Result)
41
+ attribute :has_more, Types::Bool
42
+
43
+ def self.from_dynamic!(d)
44
+ d = Types::Hash[d]
45
+ new(
46
+ results: d.fetch("results").map { |x| Result.from_dynamic!(x) },
47
+ has_more: d.fetch("has_more")
48
+ )
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "json"
2
4
  require "dry/struct"
3
5
 
data/lib/mihari/types.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "dry/types"
2
4
 
3
5
  module Mihari
@@ -8,17 +10,22 @@ module Mihari
8
10
  Nil = Strict::Nil
9
11
  Hash = Strict::Hash
10
12
  String = Strict::String
13
+ Bool = Strict::Bool
11
14
  Double = Strict::Float | Strict::Integer
12
15
  DateTime = Strict::DateTime
13
16
 
14
17
  DataTypes = Types::String.enum(*ALLOWED_DATA_TYPES)
15
18
 
19
+ UrlscanDataTypes = Types::String.enum("ip", "domain", "url")
20
+
16
21
  AnalyzerTypes = Types::String.enum(
17
22
  "binaryedge",
18
23
  "censys",
19
24
  "circl",
20
25
  "dnpedia",
21
26
  "dnstwister",
27
+ "feed",
28
+ "greynoise",
22
29
  "onyphe",
23
30
  "otx",
24
31
  "passivetotal",
@@ -32,5 +39,8 @@ module Mihari
32
39
  "vt_intel",
33
40
  "vt"
34
41
  )
42
+
43
+ FeedHttpRequestMethods = Types::String.enum("GET", "POST")
44
+ FeedHttpRequestPayloadTypes = Types::String.enum("application/json", "application/x-www-form-urlencoded")
35
45
  end
36
46
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mihari
4
- VERSION = "3.9.2"
4
+ VERSION = "3.12.0"
5
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Entities
2
4
  require "mihari/web/entities/message"
3
5
 
@@ -14,10 +14,10 @@ module Mihari
14
14
  expose :whois_record, using: Entities::WhoisRecord, documentation: { type: Entities::WhoisRecord, required: false }, as: :whoisRecord
15
15
 
16
16
  expose :reverse_dns_names, using: Entities::ReverseDnsName, documentation: { type: Entities::ReverseDnsName, is_array: true, required: false }, as: :reverseDnsNames do |status, _options|
17
- status.reverse_dns_names.length > 0 ? status.reverse_dns_names : nil
17
+ status.reverse_dns_names.empty? ? nil : status.reverse_dns_names
18
18
  end
19
19
  expose :dns_records, using: Entities::DnsRecord, documentation: { type: Entities::DnsRecord, is_array: true, required: false }, as: :dnsRecords do |status, _options|
20
- status.dns_records.length > 0 ? status.dns_records : nil
20
+ status.dns_records.empty? ? nil : status.dns_records
21
21
  end
22
22
  end
23
23
  end
@@ -9,7 +9,7 @@ module Mihari
9
9
  expose :expires_on, documentation: { type: Date, required: false }, as: :expiresOn
10
10
  expose :registrar, documentation: { type: Hash, required: false }
11
11
  expose :contacts, documentation: { type: Hash, is_array: true, required: true } do |whois_record, _options|
12
- whois_record.contacts.map { |h| h.to_camelback_keys }
12
+ whois_record.contacts.map(&:to_camelback_keys)
13
13
  end
14
14
  end
15
15
  end
@@ -1 +1 @@
1
- <!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/static/favicon.ico"><title>Mihari</title><link href="/static/js/app.14008741.js" rel="preload" as="script"></head><body><noscript><strong>We're sorry but Mihari doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/static/js/app.14008741.js"></script></body></html>
1
+ <!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/static/favicon.ico"><title>Mihari</title><link href="/static/js/app.5dc97aae.js" rel="preload" as="script"></head><body><noscript><strong>We're sorry but Mihari doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/static/js/app.5dc97aae.js"></script></body></html>