mihari 3.6.0 → 3.6.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.
Files changed (117) hide show
  1. checksums.yaml +4 -4
  2. data/.gitmodules +3 -0
  3. data/README.md +2 -0
  4. data/Steepfile +32 -0
  5. data/lib/mihari/analyzers/base.rb +5 -5
  6. data/lib/mihari/analyzers/binaryedge.rb +13 -0
  7. data/lib/mihari/analyzers/censys.rb +5 -0
  8. data/lib/mihari/analyzers/circl.rb +15 -0
  9. data/lib/mihari/analyzers/crtsh.rb +5 -0
  10. data/lib/mihari/analyzers/dnpedia.rb +5 -0
  11. data/lib/mihari/analyzers/dnstwister.rb +17 -0
  12. data/lib/mihari/analyzers/onyphe.rb +20 -4
  13. data/lib/mihari/analyzers/otx.rb +20 -0
  14. data/lib/mihari/analyzers/passivetotal.rb +25 -0
  15. data/lib/mihari/analyzers/pulsedive.rb +10 -0
  16. data/lib/mihari/analyzers/rule.rb +18 -0
  17. data/lib/mihari/analyzers/securitytrails.rb +25 -0
  18. data/lib/mihari/analyzers/shodan.rb +13 -0
  19. data/lib/mihari/analyzers/spyse.rb +20 -0
  20. data/lib/mihari/analyzers/urlscan.rb +10 -0
  21. data/lib/mihari/analyzers/virustotal.rb +20 -0
  22. data/lib/mihari/analyzers/zoomeye.rb +38 -0
  23. data/lib/mihari/emitters/base.rb +1 -1
  24. data/lib/mihari/emitters/misp.rb +38 -5
  25. data/lib/mihari/emitters/slack.rb +20 -2
  26. data/lib/mihari/emitters/the_hive.rb +16 -3
  27. data/lib/mihari/emitters/webhook.rb +18 -3
  28. data/lib/mihari/mixins/disallowed_data_value.rb +1 -1
  29. data/lib/mihari/structs/onyphe.rb +2 -2
  30. data/lib/mihari/type_checker.rb +9 -9
  31. data/lib/mihari/version.rb +1 -1
  32. data/mihari.gemspec +1 -0
  33. data/sig/lib/mihari/analyzers/base.rbs +99 -0
  34. data/sig/lib/mihari/analyzers/basic.rbs +17 -0
  35. data/sig/lib/mihari/analyzers/binaryedge.rbs +25 -0
  36. data/sig/lib/mihari/analyzers/censys.rbs +38 -0
  37. data/sig/lib/mihari/analyzers/circl.rbs +29 -0
  38. data/sig/lib/mihari/analyzers/crtsh.rbs +19 -0
  39. data/sig/lib/mihari/analyzers/dnpedia.rbs +18 -0
  40. data/sig/lib/mihari/analyzers/dnstwister.rbs +27 -0
  41. data/sig/lib/mihari/analyzers/onyphe.rbs +33 -0
  42. data/sig/lib/mihari/analyzers/otx.rbs +33 -0
  43. data/sig/lib/mihari/analyzers/passivetotal.rbs +33 -0
  44. data/sig/lib/mihari/analyzers/pulsedive.rbs +27 -0
  45. data/sig/lib/mihari/analyzers/rule.rbs +68 -0
  46. data/sig/lib/mihari/analyzers/securitytrails.rbs +33 -0
  47. data/sig/lib/mihari/analyzers/shodan.rbs +33 -0
  48. data/sig/lib/mihari/analyzers/spyse.rbs +29 -0
  49. data/sig/lib/mihari/analyzers/urlscan.rbs +28 -0
  50. data/sig/lib/mihari/analyzers/virustotal.rbs +31 -0
  51. data/sig/lib/mihari/analyzers/zoomeye.rbs +33 -0
  52. data/sig/lib/mihari/cli/analyzer.rbs +39 -0
  53. data/sig/lib/mihari/cli/base.rbs +11 -0
  54. data/sig/lib/mihari/cli/init.rbs +7 -0
  55. data/sig/lib/mihari/cli/main.rbs +9 -0
  56. data/sig/lib/mihari/cli/mixins/utils.rbs +50 -0
  57. data/sig/lib/mihari/cli/validator.rbs +7 -0
  58. data/sig/lib/mihari/commands/binaryedge.rbs +7 -0
  59. data/sig/lib/mihari/commands/censys.rbs +7 -0
  60. data/sig/lib/mihari/commands/circl.rbs +7 -0
  61. data/sig/lib/mihari/commands/crtsh.rbs +7 -0
  62. data/sig/lib/mihari/commands/dnpedia.rbs +7 -0
  63. data/sig/lib/mihari/commands/dnstwister.rbs +7 -0
  64. data/sig/lib/mihari/commands/init.rbs +11 -0
  65. data/sig/lib/mihari/commands/json.rbs +7 -0
  66. data/sig/lib/mihari/commands/onyphe.rbs +7 -0
  67. data/sig/lib/mihari/commands/otx.rbs +7 -0
  68. data/sig/lib/mihari/commands/passivetotal.rbs +7 -0
  69. data/sig/lib/mihari/commands/pulsedive.rbs +7 -0
  70. data/sig/lib/mihari/commands/search.rbs +35 -0
  71. data/sig/lib/mihari/commands/securitytrails.rbs +7 -0
  72. data/sig/lib/mihari/commands/shodan.rbs +7 -0
  73. data/sig/lib/mihari/commands/spyse.rbs +7 -0
  74. data/sig/lib/mihari/commands/urlscan.rbs +7 -0
  75. data/sig/lib/mihari/commands/validator.rbs +11 -0
  76. data/sig/lib/mihari/commands/virustotal.rbs +7 -0
  77. data/sig/lib/mihari/commands/web.rbs +7 -0
  78. data/sig/lib/mihari/commands/zoomeye.rbs +7 -0
  79. data/sig/lib/mihari/constants.rbs +3 -0
  80. data/sig/lib/mihari/database.rbs +25 -0
  81. data/sig/lib/mihari/emitters/base.rbs +18 -0
  82. data/sig/lib/mihari/emitters/database.rbs +9 -0
  83. data/sig/lib/mihari/emitters/misp.rbs +28 -0
  84. data/sig/lib/mihari/emitters/slack.rbs +58 -0
  85. data/sig/lib/mihari/emitters/stdout.rbs +9 -0
  86. data/sig/lib/mihari/emitters/the_hive.rbs +24 -0
  87. data/sig/lib/mihari/emitters/webhook.rbs +20 -0
  88. data/sig/lib/mihari/errors.rbs +10 -0
  89. data/sig/lib/mihari/mixins/configurable.rbs +26 -0
  90. data/sig/lib/mihari/mixins/configuration.rbs +45 -0
  91. data/sig/lib/mihari/mixins/disallowed_data_value.rbs +25 -0
  92. data/sig/lib/mihari/mixins/hash.rbs +14 -0
  93. data/sig/lib/mihari/mixins/refang.rbs +14 -0
  94. data/sig/lib/mihari/mixins/retriable.rbs +15 -0
  95. data/sig/lib/mihari/mixins/rule.rbs +41 -0
  96. data/sig/lib/mihari/models/alert.rbs +46 -0
  97. data/sig/lib/mihari/models/artifact.rbs +54 -0
  98. data/sig/lib/mihari/models/autonomous_system.rbs +5 -0
  99. data/sig/lib/mihari/models/dns.rbs +19 -0
  100. data/sig/lib/mihari/models/geolocation.rbs +6 -0
  101. data/sig/lib/mihari/models/reverse_dns.rbs +14 -0
  102. data/sig/lib/mihari/models/tag.rbs +5 -0
  103. data/sig/lib/mihari/models/tagging.rbs +4 -0
  104. data/sig/lib/mihari/models/whois.rbs +66 -0
  105. data/sig/lib/mihari/notifiers/base.rbs +18 -0
  106. data/sig/lib/mihari/notifiers/exception_notifier.rbs +75 -0
  107. data/sig/lib/mihari/notifiers/slack.rbs +50 -0
  108. data/sig/lib/mihari/status.rbs +25 -0
  109. data/sig/lib/mihari/structs/censys.rbs +50 -0
  110. data/sig/lib/mihari/structs/onyphe.rbs +25 -0
  111. data/sig/lib/mihari/structs/shodan.rbs +28 -0
  112. data/sig/lib/mihari/type_checker.rbs +48 -0
  113. data/sig/lib/mihari/types.rbs +17 -0
  114. data/sig/lib/mihari/version.rbs +3 -0
  115. data/sig/lib/mihari/web/app.rbs +5 -0
  116. data/sig/lib/mihari.rbs +57 -0
  117. metadata +102 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c9c1cbdf0570c25e2d89d7f6fd402b64991dfaebc75cf3cf5422a56504287ae9
4
- data.tar.gz: d5b7a0db7b49f245e3c949135011fb674e28a9d3c251e165f827e8f3d90673b1
3
+ metadata.gz: 14d0c74e85fbf6ef624afefe7e948595586d6c00fa8bc32f211e60caee581fc3
4
+ data.tar.gz: 9d5fde6d69f664efac0d6c56e6a0ba60adcad0edcfe45c69f285ffcaba8d11f0
5
5
  SHA512:
6
- metadata.gz: e76a216dedbc1aec17748c37a1b874c2c825fed6f7716ef356a48ddf2861584da299c384737e588a48b67165874f495192bb42a4c20c2f29f4620f8b559d1a83
7
- data.tar.gz: 2f3e380b252ba238594ccacd2df8e362a62b9685aafeff34d6506e7301184cf5b38c4244db9498842f4aee9df109d7c43a9ad99cecc3b63616d333a7f5093333
6
+ metadata.gz: 30597743e91f124388fbdf426199d97a00f92db9e525fe5725643d529d319ca670d1e9aa6eccff86c0844802157ca3d5c7db9b9afd925d6f0ac4d8b881c44949
7
+ data.tar.gz: 9d199a3c2f6c7794214730de7c8db812e939c5ddd11ab06d1c6f28c3b8764b2881958b0684adb59e8516d0ac4b6a1dc66b19c5a593efaac9695cb6e317ce8105
data/.gitmodules ADDED
@@ -0,0 +1,3 @@
1
+ [submodule "vendor/rbs/gem_rbs_collection"]
2
+ path = vendor/rbs/gem_rbs_collection
3
+ url = https://github.com/ruby/gem_rbs_collection.git
data/README.md CHANGED
@@ -64,3 +64,5 @@ The gem is available as open source under the terms of the [MIT License](https:/
64
64
  ## Acknowledgement
65
65
 
66
66
  Mihari is proudly supported by [Tines.io](https://tines.io?utm_source=github&utm_medium=sponsorship&utm_campaign=ninoseki), The SOAR Platform for Enterprise Security Teams.
67
+
68
+ $ bundle exec rbs -rpathname --repo=gem_rbs/gems -ractivesupport -ractionpack -ractivejob -ractivemodel -ractionview -ractiverecord -rrailties -I sig validate
data/Steepfile ADDED
@@ -0,0 +1,32 @@
1
+ target :lib do
2
+ signature "sig"
3
+ check "lib"
4
+
5
+ repo_path "vendor/rbs/gem_rbs_collection/gems"
6
+
7
+ library "date"
8
+ library "json"
9
+ library "logger"
10
+ library "monitor"
11
+ library "mutex_m"
12
+ library "pathname"
13
+ library "securerandom"
14
+ library "singleton"
15
+ library "time"
16
+ library "tsort"
17
+ library "uri"
18
+ library "resolv"
19
+ library "timeout"
20
+ library "socket"
21
+
22
+ library "rack"
23
+
24
+ library "actionpack"
25
+ library "actionview"
26
+ library "activejob"
27
+ library "activemodel"
28
+ library "activerecord"
29
+ library "activesupport"
30
+ library "parallel"
31
+ library "railties"
32
+ end
@@ -27,7 +27,7 @@ module Mihari
27
27
 
28
28
  # @return [String]
29
29
  def title
30
- self.class.to_s.split("::").last
30
+ self.class.to_s.split("::").last.to_s
31
31
  end
32
32
 
33
33
  # @return [String]
@@ -37,7 +37,7 @@ module Mihari
37
37
 
38
38
  # @return [String]
39
39
  def source
40
- self.class.to_s.split("::").last
40
+ self.class.to_s.split("::").last.to_s
41
41
  end
42
42
 
43
43
  # @return [Array<String>]
@@ -125,9 +125,9 @@ module Mihari
125
125
  #
126
126
  def set_enriched_artifacts
127
127
  retry_on_error { enriched_artifacts }
128
- rescue ArgumentError => _e
128
+ rescue ArgumentError => e
129
129
  klass = self.class.to_s.split("::").last.to_s
130
- raise Error, "Please configure #{klass} API settings properly"
130
+ raise Error, "Please configure #{klass} settings properly. (#{e})"
131
131
  end
132
132
 
133
133
  #
@@ -139,7 +139,7 @@ module Mihari
139
139
  @valid_emitters ||= Mihari.emitters.filter_map do |klass|
140
140
  emitter = klass.new
141
141
  emitter.valid? ? emitter : nil
142
- end
142
+ end.compact
143
143
  end
144
144
 
145
145
  #
@@ -26,6 +26,14 @@ module Mihari
26
26
 
27
27
  PAGE_SIZE = 20
28
28
 
29
+ #
30
+ # Search with pagination
31
+ #
32
+ # @param [String] query
33
+ # @param [Integer] page
34
+ #
35
+ # @return [Hash]
36
+ #
29
37
  def search_with_page(query, page: 1)
30
38
  api.host.search(query, page: page)
31
39
  rescue ::BinaryEdge::Error => e
@@ -34,6 +42,11 @@ module Mihari
34
42
  raise e
35
43
  end
36
44
 
45
+ #
46
+ # Search
47
+ #
48
+ # @return [Array<Hash>]
49
+ #
37
50
  def search
38
51
  responses = []
39
52
  (1..Float::INFINITY).each do |page|
@@ -16,6 +16,11 @@ module Mihari
16
16
 
17
17
  private
18
18
 
19
+ #
20
+ # Search
21
+ #
22
+ # @return [Array<String>]
23
+ #
19
24
  def search
20
25
  artifacts = []
21
26
 
@@ -35,6 +35,11 @@ module Mihari
35
35
  @api ||= ::PassiveCIRCL::API.new(username: Mihari.config.circl_passive_username, password: Mihari.config.circl_passive_password)
36
36
  end
37
37
 
38
+ #
39
+ # Passive DNS/SSL search
40
+ #
41
+ # @return [Array<String>]
42
+ #
38
43
  def search
39
44
  case @type
40
45
  when "domain"
@@ -46,6 +51,11 @@ module Mihari
46
51
  end
47
52
  end
48
53
 
54
+ #
55
+ # Passive DNS search
56
+ #
57
+ # @return [Array<String>]
58
+ #
49
59
  def passive_dns_search
50
60
  results = api.dns.query(@query)
51
61
  results.filter_map do |result|
@@ -54,6 +64,11 @@ module Mihari
54
64
  end.uniq
55
65
  end
56
66
 
67
+ #
68
+ # Passive SSL search
69
+ #
70
+ # @return [Array<String>]
71
+ #
57
72
  def passive_ssl_search
58
73
  result = api.ssl.cquery(@query)
59
74
  seen = result["seen"] || []
@@ -23,6 +23,11 @@ module Mihari
23
23
  @api ||= ::Crtsh::API.new
24
24
  end
25
25
 
26
+ #
27
+ # Search
28
+ #
29
+ # @return [Array<Hash>]
30
+ #
26
31
  def search
27
32
  exclude = exclude_expired ? "expired" : nil
28
33
  api.search(query, exclude: exclude)
@@ -20,6 +20,11 @@ module Mihari
20
20
  @api ||= ::DNPedia::API.new
21
21
  end
22
22
 
23
+ #
24
+ # Search
25
+ #
26
+ # @return [Array<String>]
27
+ #
23
28
  def search
24
29
  res = api.search(query)
25
30
  rows = res["rows"] || []
@@ -29,6 +29,11 @@ module Mihari
29
29
 
30
30
  private
31
31
 
32
+ #
33
+ # Check whether a type is valid or not
34
+ #
35
+ # @return [Boolean]
36
+ #
32
37
  def valid_type?
33
38
  type == "domain"
34
39
  end
@@ -37,6 +42,13 @@ module Mihari
37
42
  @api ||= ::DNSTwister::API.new
38
43
  end
39
44
 
45
+ #
46
+ # Check whether a domain is resolvable or not
47
+ #
48
+ # @param [String] domain
49
+ #
50
+ # @return [Boolean]
51
+ #
40
52
  def resolvable?(domain)
41
53
  Resolv.getaddress domain
42
54
  true
@@ -44,6 +56,11 @@ module Mihari
44
56
  false
45
57
  end
46
58
 
59
+ #
60
+ # Search
61
+ #
62
+ # @return [Array<String>]
63
+ #
47
64
  def search
48
65
  raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
49
66
 
@@ -33,11 +33,24 @@ module Mihari
33
33
  @api ||= ::Onyphe::API.new(Mihari.config.onyphe_api_key)
34
34
  end
35
35
 
36
+ #
37
+ # Search with pagination
38
+ #
39
+ # @param [String] query
40
+ # @param [Integer] page
41
+ #
42
+ # @return [Structs::Onyphe::Response]
43
+ #
36
44
  def search_with_page(query, page: 1)
37
45
  res = api.simple.datascan(query, page: page)
38
46
  Structs::Onyphe::Response.from_dynamic!(res)
39
47
  end
40
48
 
49
+ #
50
+ # Search
51
+ #
52
+ # @return [Array<Structs::Onyphe::Response>]
53
+ #
41
54
  def search
42
55
  responses = []
43
56
  (1..Float::INFINITY).each do |page|
@@ -60,10 +73,13 @@ module Mihari
60
73
  def build_artifact(result)
61
74
  as = AutonomousSystem.new(asn: normalize_asn(result.asn))
62
75
 
63
- geolocation = Geolocation.new(
64
- country: NormalizeCountry(result.country_code, to: :short),
65
- country_code: result.country_code
66
- )
76
+ geolocation = nil
77
+ unless result.country_code.nil?
78
+ geolocation = Geolocation.new(
79
+ country: NormalizeCountry(result.country_code, to: :short),
80
+ country_code: result.country_code
81
+ )
82
+ end
67
83
 
68
84
  Artifact.new(
69
85
  data: result.ip,
@@ -39,10 +39,20 @@ module Mihari
39
39
  @ip_client ||= ::OTX::IP.new(Mihari.config.otx_api_key)
40
40
  end
41
41
 
42
+ #
43
+ # Check whether a type is valid or not
44
+ #
45
+ # @return [Boolean]
46
+ #
42
47
  def valid_type?
43
48
  %w[ip domain].include? type
44
49
  end
45
50
 
51
+ #
52
+ # IP/domain search
53
+ #
54
+ # @return [Array<String>]
55
+ #
46
56
  def search
47
57
  case type
48
58
  when "domain"
@@ -54,6 +64,11 @@ module Mihari
54
64
  end
55
65
  end
56
66
 
67
+ #
68
+ # Domain search
69
+ #
70
+ # @return [Array<String>]
71
+ #
57
72
  def domain_search
58
73
  records = domain_client.get_passive_dns(query)
59
74
  records.filter_map do |record|
@@ -61,6 +76,11 @@ module Mihari
61
76
  end.uniq
62
77
  end
63
78
 
79
+ #
80
+ # IP search
81
+ #
82
+ # @return [Array<String>]
83
+ #
64
84
  def ip_search
65
85
  records = ip_client.get_passive_dns(query)
66
86
  records.filter_map do |record|
@@ -35,10 +35,20 @@ module Mihari
35
35
  @api ||= ::PassiveTotal::API.new(username: Mihari.config.passivetotal_username, api_key: Mihari.config.passivetotal_api_key)
36
36
  end
37
37
 
38
+ #
39
+ # Check whether a type is valid or not
40
+ #
41
+ # @return [Boolean]
42
+ #
38
43
  def valid_type?
39
44
  %w[ip domain mail hash].include? type
40
45
  end
41
46
 
47
+ #
48
+ # Passive DNS/SSL, reverse whois search
49
+ #
50
+ # @return [Array<String>]
51
+ #
42
52
  def search
43
53
  case type
44
54
  when "domain", "ip"
@@ -52,11 +62,21 @@ module Mihari
52
62
  end
53
63
  end
54
64
 
65
+ #
66
+ # Passive DNS search
67
+ #
68
+ # @return [Array<String>]
69
+ #
55
70
  def passive_dns_search
56
71
  res = api.dns.passive_unique(query)
57
72
  res["results"] || []
58
73
  end
59
74
 
75
+ #
76
+ # Reverse whois search
77
+ #
78
+ # @return [Array<String>]
79
+ #
60
80
  def reverse_whois_search
61
81
  res = api.whois.search(query: query, field: "email")
62
82
  results = res["results"] || []
@@ -65,6 +85,11 @@ module Mihari
65
85
  end.flatten.compact.uniq
66
86
  end
67
87
 
88
+ #
89
+ # Passive SSL search
90
+ #
91
+ # @return [Array<String>]
92
+ #
68
93
  def ssl_search
69
94
  res = api.ssl.history(query)
70
95
  results = res["results"] || []
@@ -35,10 +35,20 @@ module Mihari
35
35
  @api ||= ::Pulsedive::API.new(Mihari.config.pulsedive_api_key)
36
36
  end
37
37
 
38
+ #
39
+ # Check whether a type is valid or not
40
+ #
41
+ # @return [Boolean]
42
+ #
38
43
  def valid_type?
39
44
  %w[ip domain].include? type
40
45
  end
41
46
 
47
+ #
48
+ # Search
49
+ #
50
+ # @return [Array<String>]
51
+ #
42
52
  def search
43
53
  raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
44
54
 
@@ -22,6 +22,8 @@ module Mihari
22
22
  super(**kwargs)
23
23
 
24
24
  @source = id || UUIDTools::UUID.md5_create(UUIDTools::UUID_URL_NAMESPACE, title + description).to_s
25
+
26
+ validate_analyzer_configurations
25
27
  end
26
28
 
27
29
  ANALYZER_TO_CLASS = {
@@ -119,6 +121,22 @@ module Mihari
119
121
 
120
122
  raise ArgumentError, "#{analyzer_name} is not supported"
121
123
  end
124
+
125
+ #
126
+ # Validate configuration of analyzers
127
+ #
128
+ def validate_analyzer_configurations
129
+ queries.each do |params|
130
+ analyzer_name = params[:analyzer]
131
+ klass = get_analyzer_class(analyzer_name)
132
+
133
+ instance = klass.new("dummy")
134
+ unless instance.configured?
135
+ klass_name = klass.to_s.split("::").last
136
+ raise ArgumentError, "#{klass_name} is not configured correctly"
137
+ end
138
+ end
139
+ end
122
140
  end
123
141
  end
124
142
  end
@@ -35,10 +35,20 @@ module Mihari
35
35
  @api ||= ::SecurityTrails::API.new(Mihari.config.securitytrails_api_key)
36
36
  end
37
37
 
38
+ #
39
+ # Check whether a type is valid or not
40
+ #
41
+ # @return [Boolean]
42
+ #
38
43
  def valid_type?
39
44
  %w[ip domain mail].include? type
40
45
  end
41
46
 
47
+ #
48
+ # IP/domain/mail search
49
+ #
50
+ # @return [Array<String>]
51
+ #
42
52
  def search
43
53
  case type
44
54
  when "domain"
@@ -52,6 +62,11 @@ module Mihari
52
62
  end
53
63
  end
54
64
 
65
+ #
66
+ # Domain search
67
+ #
68
+ # @return [Array<String>]
69
+ #
55
70
  def domain_search
56
71
  result = api.history.get_all_dns_history(query, type: "a")
57
72
  records = result["records"] || []
@@ -60,12 +75,22 @@ module Mihari
60
75
  end.flatten.compact.uniq
61
76
  end
62
77
 
78
+ #
79
+ # IP search
80
+ #
81
+ # @return [Array<String>]
82
+ #
63
83
  def ip_search
64
84
  result = api.domains.search(filter: { ipv4: query })
65
85
  records = result["records"] || []
66
86
  records.filter_map { |record| record["hostname"] }.uniq
67
87
  end
68
88
 
89
+ #
90
+ # Mail search
91
+ #
92
+ # @return [Array<String>]
93
+ #
69
94
  def mail_search
70
95
  result = api.domains.search(filter: { whois_email: query })
71
96
  records = result["records"] || []