mihari 5.4.1 → 5.4.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/frontend/package-lock.json +145 -146
  3. data/frontend/package.json +8 -8
  4. data/frontend/src/swagger.yaml +306 -272
  5. data/lib/mihari/analyzers/base.rb +0 -4
  6. data/lib/mihari/analyzers/binaryedge.rb +4 -44
  7. data/lib/mihari/analyzers/censys.rb +4 -20
  8. data/lib/mihari/analyzers/circl.rb +2 -26
  9. data/lib/mihari/analyzers/crtsh.rb +2 -17
  10. data/lib/mihari/analyzers/dnstwister.rb +1 -3
  11. data/lib/mihari/analyzers/greynoise.rb +5 -4
  12. data/lib/mihari/analyzers/hunterhow.rb +8 -23
  13. data/lib/mihari/analyzers/onyphe.rb +5 -39
  14. data/lib/mihari/analyzers/otx.rb +2 -38
  15. data/lib/mihari/analyzers/passivetotal.rb +3 -41
  16. data/lib/mihari/analyzers/securitytrails.rb +3 -41
  17. data/lib/mihari/analyzers/shodan.rb +7 -39
  18. data/lib/mihari/analyzers/urlscan.rb +2 -38
  19. data/lib/mihari/analyzers/virustotal_intelligence.rb +2 -25
  20. data/lib/mihari/analyzers/zoomeye.rb +17 -83
  21. data/lib/mihari/cli/alert.rb +11 -0
  22. data/lib/mihari/cli/main.rb +6 -1
  23. data/lib/mihari/clients/base.rb +9 -1
  24. data/lib/mihari/clients/binaryedge.rb +27 -2
  25. data/lib/mihari/clients/censys.rb +32 -2
  26. data/lib/mihari/clients/circl.rb +28 -1
  27. data/lib/mihari/clients/crtsh.rb +9 -2
  28. data/lib/mihari/clients/dnstwister.rb +4 -2
  29. data/lib/mihari/clients/greynoise.rb +31 -4
  30. data/lib/mihari/clients/hunterhow.rb +41 -3
  31. data/lib/mihari/clients/onyphe.rb +25 -3
  32. data/lib/mihari/clients/otx.rb +40 -0
  33. data/lib/mihari/clients/passivetotal.rb +33 -15
  34. data/lib/mihari/clients/securitytrails.rb +44 -0
  35. data/lib/mihari/clients/shodan.rb +30 -2
  36. data/lib/mihari/clients/urlscan.rb +32 -6
  37. data/lib/mihari/clients/virustotal.rb +29 -4
  38. data/lib/mihari/clients/zoomeye.rb +53 -2
  39. data/lib/mihari/commands/alert.rb +42 -0
  40. data/lib/mihari/commands/rule.rb +2 -2
  41. data/lib/mihari/commands/search.rb +20 -59
  42. data/lib/mihari/commands/web.rb +1 -1
  43. data/lib/mihari/config.rb +2 -2
  44. data/lib/mihari/emitters/base.rb +1 -1
  45. data/lib/mihari/emitters/database.rb +2 -2
  46. data/lib/mihari/errors.rb +23 -2
  47. data/lib/mihari/http.rb +7 -1
  48. data/lib/mihari/schemas/alert.rb +14 -0
  49. data/lib/mihari/services/alert_proxy.rb +106 -0
  50. data/lib/mihari/services/alert_runner.rb +22 -0
  51. data/lib/mihari/services/{rule.rb → rule_proxy.rb} +10 -6
  52. data/lib/mihari/services/rule_runner.rb +49 -0
  53. data/lib/mihari/structs/censys.rb +11 -11
  54. data/lib/mihari/structs/greynoise.rb +17 -8
  55. data/lib/mihari/structs/onyphe.rb +7 -7
  56. data/lib/mihari/structs/shodan.rb +5 -5
  57. data/lib/mihari/structs/urlscan.rb +3 -3
  58. data/lib/mihari/structs/virustotal_intelligence.rb +3 -3
  59. data/lib/mihari/version.rb +1 -1
  60. data/lib/mihari/web/endpoints/alerts.rb +22 -0
  61. data/lib/mihari/web/endpoints/rules.rb +8 -8
  62. data/lib/mihari/web/public/assets/{index-61dc587c.js → index-4d7eda9f.js} +1 -1
  63. data/lib/mihari/web/public/index.html +1 -1
  64. data/lib/mihari/web/public/redoc-static.html +29 -27
  65. data/lib/mihari.rb +6 -1
  66. data/mihari.gemspec +9 -10
  67. metadata +28 -37
  68. data/Steepfile +0 -31
@@ -82,10 +82,6 @@ module Mihari
82
82
 
83
83
  private
84
84
 
85
- def sleep_interval
86
- sleep(interval) if interval
87
- end
88
-
89
85
  class << self
90
86
  #
91
87
  # Initialize an analyzer by query params
@@ -18,16 +18,13 @@ module Mihari
18
18
  end
19
19
 
20
20
  def artifacts
21
- results = search
22
- return [] unless results || results.empty?
23
-
24
- results.map do |result|
25
- events = result["events"] || []
21
+ client.search_with_pagination(query, pagination_limit: pagination_limit).map do |res|
22
+ events = res["events"] || []
26
23
  events.filter_map do |event|
27
24
  data = event.dig("target", "ip")
28
25
  data.nil? ? nil : Artifact.new(data: data, source: source, metadata: event)
29
26
  end
30
- end.flatten
27
+ end
31
28
  end
32
29
 
33
30
  def configuration_keys
@@ -36,49 +33,12 @@ module Mihari
36
33
 
37
34
  private
38
35
 
39
- PAGE_SIZE = 20
40
-
41
- #
42
- # Search with pagination
43
- #
44
- # @param [Integer] page
45
- #
46
- # @return [Hash]
47
- #
48
- def search_with_page(page: 1)
49
- client.search(query, page: page)
50
- rescue StatusCodeError => e
51
- raise RetryableError, e if e.message.include?("Request time limit exceeded")
52
-
53
- raise e
54
- end
55
-
56
- #
57
- # Search
58
- #
59
- # @return [Array<Hash>]
60
- #
61
- def search
62
- responses = []
63
- (1..pagination_limit).each do |page|
64
- res = search_with_page(page: page)
65
- total = res["total"].to_i
66
-
67
- responses << res
68
- break if total <= page * PAGE_SIZE
69
-
70
- # sleep #{interval} seconds to avoid the rate limitation (if it is set)
71
- sleep_interval
72
- end
73
- responses
74
- end
75
-
76
36
  #
77
37
  #
78
38
  # @return [Mihari::Clients::BinaryEdge]
79
39
  #
80
40
  def client
81
- @client ||= Clients::BinaryEdge.new(api_key: api_key)
41
+ @client ||= Clients::BinaryEdge.new(api_key: api_key, interval: interval)
82
42
  end
83
43
  end
84
44
  end
@@ -27,25 +27,9 @@ module Mihari
27
27
  # @return [Array<Mihari::Artifact>]
28
28
  #
29
29
  def artifacts
30
- artifacts = []
31
-
32
- cursor = nil
33
- pagination_limit.times do
34
- response = client.search(query, cursor: cursor)
35
- artifacts << response.result.to_artifacts
36
- cursor = response.result.links.next
37
- # NOTE: Censys's search API is unstable recently
38
- # it may returns empty links or empty string cursors
39
- # - Empty links: "links": {}
40
- # - Empty cursors: "links": { "next": "", "prev": "" }
41
- # So it needs to check both cases
42
- break if cursor.nil? || cursor.empty?
43
-
44
- # sleep #{interval} seconds to avoid the rate limitation (if it is set)
45
- sleep_interval
46
- end
47
-
48
- artifacts.flatten.uniq(&:data)
30
+ client.search_with_pagination(query, pagination_limit: pagination_limit).map do |res|
31
+ res.result.artifacts
32
+ end.flatten.uniq(&:data)
49
33
  end
50
34
 
51
35
  #
@@ -68,7 +52,7 @@ module Mihari
68
52
  # @return [Mihari::Clients::Censys]
69
53
  #
70
54
  def client
71
- @client ||= Clients::Censys.new(id: id, secret: secret)
55
+ @client ||= Clients::Censys.new(id: id, secret: secret, interval: interval)
72
56
  end
73
57
 
74
58
  #
@@ -32,9 +32,9 @@ module Mihari
32
32
  def artifacts
33
33
  case type
34
34
  when "domain"
35
- passive_dns_search
35
+ client.passive_dns_search query
36
36
  when "hash"
37
- passive_ssl_search
37
+ client.passive_ssl_search query
38
38
  else
39
39
  raise InvalidInputError, "#{@query}(type: #{@type || "unknown"}) is not supported."
40
40
  end
@@ -54,30 +54,6 @@ module Mihari
54
54
  @client ||= Clients::CIRCL.new(username: username, password: password)
55
55
  end
56
56
 
57
- #
58
- # Passive DNS search
59
- #
60
- # @return [Array<String>]
61
- #
62
- def passive_dns_search
63
- results = client.dns_query(query)
64
- results.filter_map do |result|
65
- type = result["rrtype"]
66
- (type == "A") ? result["rdata"] : nil
67
- end.uniq
68
- end
69
-
70
- #
71
- # Passive SSL search
72
- #
73
- # @return [Array<String>]
74
- #
75
- def passive_ssl_search
76
- result = client.ssl_cquery(query)
77
- seen = result["seen"] || []
78
- seen.uniq
79
- end
80
-
81
57
  def username?
82
58
  !username.nil?
83
59
  end
@@ -18,13 +18,8 @@ module Mihari
18
18
  end
19
19
 
20
20
  def artifacts
21
- results = search
22
- results.map do |result|
23
- values = result["name_value"].to_s.lines.map(&:chomp)
24
- values.map do |value|
25
- Artifact.new(data: value, source: source, metadata: result)
26
- end
27
- end.flatten
21
+ exclude = exclude_expired ? "expired" : nil
22
+ client.search(query, exclude: exclude)
28
23
  end
29
24
 
30
25
  private
@@ -35,16 +30,6 @@ module Mihari
35
30
  def client
36
31
  @client ||= Mihari::Clients::Crtsh.new
37
32
  end
38
-
39
- #
40
- # Search
41
- #
42
- # @return [Array<Hash>]
43
- #
44
- def search
45
- exclude = exclude_expired ? "expired" : nil
46
- client.search(query, exclude: exclude)
47
- end
48
33
  end
49
34
  end
50
35
  end
@@ -21,9 +21,7 @@ module Mihari
21
21
  def artifacts
22
22
  raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
23
23
 
24
- res = client.fuzz(query)
25
- fuzzy_domains = res["fuzzy_domains"] || []
26
- domains = fuzzy_domains.map { |domain| domain["domain"] }
24
+ domains = client.fuzz(query)
27
25
  Parallel.map(domains) do |domain|
28
26
  resolvable?(domain) ? domain : nil
29
27
  end.compact
@@ -3,8 +3,6 @@
3
3
  module Mihari
4
4
  module Analyzers
5
5
  class GreyNoise < Base
6
- PAGE_SIZE = 10_000
7
-
8
6
  # @return [String, nil]
9
7
  attr_reader :api_key
10
8
 
@@ -20,7 +18,10 @@ module Mihari
20
18
  end
21
19
 
22
20
  def artifacts
23
- client.gnql_search(query, size: PAGE_SIZE).to_artifacts
21
+ client.gnql_search_with_pagination(
22
+ query,
23
+ pagination_limit: pagination_limit
24
+ ).map(&:artifacts).flatten
24
25
  end
25
26
 
26
27
  def configuration_keys
@@ -30,7 +31,7 @@ module Mihari
30
31
  private
31
32
 
32
33
  def client
33
- @client ||= Clients::GreyNoise.new(api_key: api_key)
34
+ @client ||= Clients::GreyNoise.new(api_key: api_key, interval: interval)
34
35
  end
35
36
  end
36
37
  end
@@ -30,25 +30,13 @@ module Mihari
30
30
  # @return [Array<Mihari::Artifact>]
31
31
  #
32
32
  def artifacts
33
- artifacts = []
34
-
35
- (1..pagination_limit).each do |page|
36
- res = client.search(
37
- query,
38
- page: page,
39
- page_size: PAGE_SIZE,
40
- start_time: start_time.strftime("%Y-%m-%d"),
41
- end_time: end_time.strftime("%Y-%m-%d")
42
- )
43
-
44
- artifacts << res.data.artifacts
45
-
46
- break if res.data.list.length < PAGE_SIZE
47
-
48
- sleep_interval
49
- end
50
-
51
- artifacts.flatten
33
+ client.search_with_pagination(
34
+ query,
35
+ start_time: start_time.strftime("%Y-%m-%d"),
36
+ end_time: end_time.strftime("%Y-%m-%d")
37
+ ).map do |res|
38
+ res.data.artifacts
39
+ end.flatten
52
40
  end
53
41
 
54
42
  def configuration_keys
@@ -57,11 +45,8 @@ module Mihari
57
45
 
58
46
  private
59
47
 
60
- # @return [Integer]
61
- PAGE_SIZE = 100
62
-
63
48
  def client
64
- @client ||= Clients::HunterHow.new(api_key: api_key)
49
+ @client ||= Clients::HunterHow.new(api_key: api_key, interval: interval)
65
50
  end
66
51
  end
67
52
  end
@@ -20,10 +20,10 @@ module Mihari
20
20
  end
21
21
 
22
22
  def artifacts
23
- responses = search
24
- return [] unless responses
25
-
26
- responses.map(&:to_artifacts).flatten
23
+ client.datascan_with_pagination(
24
+ query,
25
+ pagination_limit: pagination_limit
26
+ ).map(&:artifacts).flatten
27
27
  end
28
28
 
29
29
  def configuration_keys
@@ -32,42 +32,8 @@ module Mihari
32
32
 
33
33
  private
34
34
 
35
- PAGE_SIZE = 10
36
-
37
35
  def client
38
- @client ||= Clients::Onyphe.new(api_key: api_key)
39
- end
40
-
41
- #
42
- # Search with pagination
43
- #
44
- # @param [String] query
45
- # @param [Integer] page
46
- #
47
- # @return [Structs::Onyphe::Response]
48
- #
49
- def search_with_page(query, page: 1)
50
- client.datascan(query, page: page)
51
- end
52
-
53
- #
54
- # Search
55
- #
56
- # @return [Array<Structs::Onyphe::Response>]
57
- #
58
- def search
59
- responses = []
60
- (1..pagination_limit).each do |page|
61
- res = search_with_page(query, page: page)
62
- responses << res
63
-
64
- total = res.total
65
- break if total <= page * PAGE_SIZE
66
-
67
- # sleep #{interval} seconds to avoid the rate limitation (if it is set)
68
- sleep_interval
69
- end
70
- responses
36
+ @client ||= Clients::Onyphe.new(api_key: api_key, interval: interval)
71
37
  end
72
38
  end
73
39
  end
@@ -27,9 +27,9 @@ module Mihari
27
27
  def artifacts
28
28
  case type
29
29
  when "domain"
30
- domain_search
30
+ client.domain_search(query)
31
31
  when "ip"
32
- ip_search
32
+ client.ip_search(query)
33
33
  else
34
34
  raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
35
35
  end
@@ -53,42 +53,6 @@ module Mihari
53
53
  def valid_type?
54
54
  %w[ip domain].include? type
55
55
  end
56
-
57
- #
58
- # Domain search
59
- #
60
- # @return [Array<String>]
61
- #
62
- def domain_search
63
- res = client.query_by_domain(query)
64
- return [] if res.nil?
65
-
66
- records = res["passive_dns"] || []
67
- records.filter_map do |record|
68
- record_type = record["record_type"]
69
- address = record["address"]
70
-
71
- address if record_type == "A"
72
- end.uniq
73
- end
74
-
75
- #
76
- # IP search
77
- #
78
- # @return [Array<String>]
79
- #
80
- def ip_search
81
- res = client.query_by_ip(query)
82
- return [] if res.nil?
83
-
84
- records = res["passive_dns"] || []
85
- records.filter_map do |record|
86
- record_type = record["record_type"]
87
- hostname = record["hostname"]
88
-
89
- hostname if record_type == "A"
90
- end.uniq
91
- end
92
56
  end
93
57
  end
94
58
  end
@@ -32,11 +32,11 @@ module Mihari
32
32
  def artifacts
33
33
  case type
34
34
  when "domain", "ip"
35
- passive_dns_search
35
+ client.passive_dns_search query
36
36
  when "mail"
37
- reverse_whois_search
37
+ client.reverse_whois_search query
38
38
  when "hash"
39
- ssl_search
39
+ client.ssl_search query
40
40
  else
41
41
  raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
42
42
  end
@@ -65,44 +65,6 @@ module Mihari
65
65
  %w[ip domain mail hash].include? type
66
66
  end
67
67
 
68
- #
69
- # Passive DNS search
70
- #
71
- # @return [Array<String>]
72
- #
73
- def passive_dns_search
74
- res = client.passive_dns_search(query)
75
- res["results"] || []
76
- end
77
-
78
- #
79
- # Reverse whois search
80
- #
81
- # @return [Array<Mihari::Artifact>]
82
- #
83
- def reverse_whois_search
84
- res = client.reverse_whois_search(query: query, field: "email")
85
- results = res["results"] || []
86
- results.map do |result|
87
- data = result["domain"]
88
- Artifact.new(data: data, source: source, metadata: result)
89
- end.flatten
90
- end
91
-
92
- #
93
- # Passive SSL search
94
- #
95
- # @return [Array<Mihari::Artifact>]
96
- #
97
- def ssl_search
98
- res = client.ssl_search(query)
99
- results = res["results"] || []
100
- results.map do |result|
101
- data = result["ipAddresses"]
102
- data.map { |d| Artifact.new(data: d, source: source, metadata: result) }
103
- end.flatten
104
- end
105
-
106
68
  def username?
107
69
  !username.nil?
108
70
  end
@@ -30,11 +30,11 @@ module Mihari
30
30
  def artifacts
31
31
  case type
32
32
  when "domain"
33
- domain_search
33
+ client.domain_search query
34
34
  when "ip"
35
- ip_search
35
+ client.ip_search query
36
36
  when "mail"
37
- mail_search
37
+ client.mail_search query
38
38
  else
39
39
  raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
40
40
  end
@@ -58,44 +58,6 @@ module Mihari
58
58
  def valid_type?
59
59
  %w[ip domain mail].include? type
60
60
  end
61
-
62
- #
63
- # Domain search
64
- #
65
- # @return [Array<String>]
66
- #
67
- def domain_search
68
- records = client.get_all_dns_history(query, type: "a")
69
- records.map do |record|
70
- (record["values"] || []).map { |value| value["ip"] }
71
- end.flatten.compact.uniq
72
- end
73
-
74
- #
75
- # IP search
76
- #
77
- # @return [Array<Mihari::Artifact>]
78
- #
79
- def ip_search
80
- records = client.search_by_ip(query)
81
- records.filter_map do |record|
82
- data = record["hostname"]
83
- Artifact.new(data: data, source: source, metadata: record)
84
- end
85
- end
86
-
87
- #
88
- # Mail search
89
- #
90
- # @return [Array<String>]
91
- #
92
- def mail_search
93
- records = client.search_by_mail(query)
94
- records.filter_map do |record|
95
- data = record["hostname"]
96
- Artifact.new(data: data, source: source, metadata: record)
97
- end
98
- end
99
61
  end
100
62
  end
101
63
  end
@@ -18,10 +18,10 @@ module Mihari
18
18
  end
19
19
 
20
20
  def artifacts
21
- results = search
22
- return [] if results.empty?
23
-
24
- results.map(&:to_artifacts).flatten.uniq(&:data)
21
+ client.search_with_pagination(
22
+ query,
23
+ pagination_limit: pagination_limit
24
+ ).map(&:artifacts).flatten.uniq(&:data)
25
25
  end
26
26
 
27
27
  def configuration_keys
@@ -30,43 +30,11 @@ module Mihari
30
30
 
31
31
  private
32
32
 
33
- PAGE_SIZE = 100
34
-
35
- def client
36
- @client ||= Clients::Shodan.new(api_key: api_key)
37
- end
38
-
39
- #
40
- # Search with pagination
41
- #
42
- # @param [Integer] page
43
- #
44
- # @return [Structs::Shodan::Result]
45
- #
46
- def search_with_page(page: 1)
47
- client.search(query, page: page)
48
- end
49
-
50
33
  #
51
- # Search
34
+ # @return [Clients::Shodan]
52
35
  #
53
- # @return [Array<Structs::Shodan::Result>]
54
- #
55
- def search
56
- responses = []
57
- (1..pagination_limit).each do |page|
58
- res = search_with_page(page: page)
59
- responses << res
60
- break if res.total <= page * PAGE_SIZE
61
-
62
- # sleep #{interval} seconds to avoid the rate limitation (if it is set)
63
- sleep_interval
64
- rescue JSON::ParserError
65
- # ignore JSON::ParserError
66
- # ref. https://github.com/ninoseki/mihari/issues/197
67
- next
68
- end
69
- responses
36
+ def client
37
+ @client ||= Clients::Shodan.new(api_key: api_key, interval: interval)
70
38
  end
71
39
  end
72
40
  end
@@ -4,7 +4,6 @@ module Mihari
4
4
  module Analyzers
5
5
  class Urlscan < Base
6
6
  SUPPORTED_DATA_TYPES = %w[url domain ip].freeze
7
- SIZE = 1000
8
7
 
9
8
  # @return [String, nil]
10
9
  attr_reader :api_key
@@ -30,9 +29,8 @@ module Mihari
30
29
  end
31
30
 
32
31
  def artifacts
33
- responses = search
34
32
  # @type [Array<Mihari::Artifact>]
35
- artifacts = responses.map { |res| res.to_artifacts }.flatten
33
+ artifacts = client.search_with_pagination(query, pagination_limit: pagination_limit).map(&:artifacts).flatten
36
34
 
37
35
  artifacts.select do |artifact|
38
36
  allowed_data_types.include? artifact.data_type
@@ -46,41 +44,7 @@ module Mihari
46
44
  private
47
45
 
48
46
  def client
49
- @client ||= Clients::UrlScan.new(api_key: api_key)
50
- end
51
-
52
- #
53
- # Search with search_after option
54
- #
55
- # @return [Structs::Urlscan::Response]
56
- #
57
- def search_with_search_after(search_after: nil)
58
- res = client.search(query, size: SIZE, search_after: search_after)
59
- Structs::Urlscan::Response.from_dynamic! res
60
- end
61
-
62
- #
63
- # Search
64
- #
65
- # @return [Array<Structs::Urlscan::Response>]
66
- #
67
- def search
68
- responses = []
69
-
70
- search_after = nil
71
- pagination_limit.times do
72
- res = search_with_search_after(search_after: search_after)
73
- responses << res
74
-
75
- break if res.results.length < SIZE
76
-
77
- search_after = res.results.last.sort.join(",")
78
-
79
- # sleep #{interval} seconds to avoid the rate limitation (if it is set)
80
- sleep_interval
81
- end
82
-
83
- responses
47
+ @client ||= Clients::UrlScan.new(api_key: api_key, interval: interval)
84
48
  end
85
49
 
86
50
  #
@@ -18,7 +18,7 @@ module Mihari
18
18
  end
19
19
 
20
20
  def artifacts
21
- search_with_cursor.map(&:to_artifacts).flatten
21
+ client.intel_search_with_pagination(query, pagination_limit: pagination_limit).map(&:artifacts).flatten
22
22
  end
23
23
 
24
24
  def configuration_keys
@@ -33,30 +33,7 @@ module Mihari
33
33
  # @return [::VirusTotal::API]
34
34
  #
35
35
  def client
36
- @client = Clients::VirusTotal.new(api_key: api_key)
37
- end
38
-
39
- #
40
- # Search with cursor
41
- #
42
- # @return [Array<Structs::VirusTotalIntelligence::Response>]
43
- #
44
- def search_with_cursor
45
- cursor = nil
46
- responses = []
47
-
48
- pagination_limit.times do
49
- response = Structs::VirusTotalIntelligence::Response.from_dynamic!(client.intel_search(query,
50
- cursor: cursor))
51
- responses << response
52
- break if response.meta.cursor.nil?
53
-
54
- cursor = response.meta.cursor
55
- # sleep #{interval} seconds to avoid the rate limitation (if it is set)
56
- sleep_interval
57
- end
58
-
59
- responses
36
+ @client = Clients::VirusTotal.new(api_key: api_key, interval: interval)
60
37
  end
61
38
  end
62
39
  end