mihari 5.4.7 → 5.4.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.rubocop.yml +2 -0
  4. data/docs/analyzers/index.md +5 -0
  5. data/frontend/package-lock.json +183 -171
  6. data/frontend/package.json +6 -6
  7. data/lib/mihari/analyzers/base.rb +8 -1
  8. data/lib/mihari/analyzers/binaryedge.rb +1 -1
  9. data/lib/mihari/analyzers/censys.rb +1 -1
  10. data/lib/mihari/analyzers/feed.rb +1 -0
  11. data/lib/mihari/analyzers/greynoise.rb +1 -1
  12. data/lib/mihari/analyzers/hunterhow.rb +1 -1
  13. data/lib/mihari/analyzers/onyphe.rb +1 -1
  14. data/lib/mihari/analyzers/shodan.rb +1 -1
  15. data/lib/mihari/analyzers/urlscan.rb +1 -1
  16. data/lib/mihari/analyzers/virustotal_intelligence.rb +1 -1
  17. data/lib/mihari/analyzers/zoomeye.rb +1 -1
  18. data/lib/mihari/clients/base.rb +18 -5
  19. data/lib/mihari/clients/binaryedge.rb +4 -3
  20. data/lib/mihari/clients/censys.rb +3 -2
  21. data/lib/mihari/clients/greynoise.rb +3 -2
  22. data/lib/mihari/clients/hunterhow.rb +3 -2
  23. data/lib/mihari/clients/onyphe.rb +4 -2
  24. data/lib/mihari/clients/shodan.rb +3 -2
  25. data/lib/mihari/clients/urlscan.rb +3 -2
  26. data/lib/mihari/clients/virustotal.rb +3 -2
  27. data/lib/mihari/clients/zoomeye.rb +3 -2
  28. data/lib/mihari/config.rb +13 -0
  29. data/lib/mihari/emitters/webhook.rb +11 -11
  30. data/lib/mihari/enrichers/google_public_dns.rb +7 -1
  31. data/lib/mihari/enrichers/ipinfo.rb +13 -6
  32. data/lib/mihari/enrichers/shodan.rb +7 -1
  33. data/lib/mihari/errors.rb +0 -2
  34. data/lib/mihari/feed/reader.rb +15 -10
  35. data/lib/mihari/http.rb +26 -100
  36. data/lib/mihari/schemas/analyzer.rb +1 -0
  37. data/lib/mihari/version.rb +1 -1
  38. data/lib/mihari/web/public/assets/{index-0a5a47bf.js → index-a92abd57.js} +1 -1
  39. data/lib/mihari/web/public/index.html +1 -1
  40. data/lib/mihari/web/public/redoc-static.html +400 -400
  41. data/mihari.gemspec +5 -4
  42. data/requirements.txt +1 -1
  43. metadata +26 -12
@@ -19,7 +19,7 @@
19
19
  "@fortawesome/vue-fontawesome": "^3.0.3",
20
20
  "@vueuse/core": "^10.5.0",
21
21
  "@vueuse/router": "^10.5.0",
22
- "ace-builds": "^1.28.0",
22
+ "ace-builds": "^1.29.0",
23
23
  "axios": "^1.5.1",
24
24
  "bulma": "^0.9.4",
25
25
  "bulma-helpers": "^0.4.3",
@@ -41,10 +41,10 @@
41
41
  "@rushstack/eslint-patch": "^1.5.1",
42
42
  "@tsconfig/node20": "^20.1.2",
43
43
  "@types/jsdom": "^21.1.3",
44
- "@types/node": "^20.8.3",
44
+ "@types/node": "^20.8.5",
45
45
  "@types/url-parse": "^1.4.9",
46
- "@typescript-eslint/eslint-plugin": "^6.7.4",
47
- "@typescript-eslint/parser": "^6.7.4",
46
+ "@typescript-eslint/eslint-plugin": "^6.7.5",
47
+ "@typescript-eslint/parser": "^6.7.5",
48
48
  "@vitejs/plugin-vue": "^4.4.0",
49
49
  "@vue/eslint-config-prettier": "^8.0.0",
50
50
  "@vue/eslint-config-typescript": "^12.0.0",
@@ -52,7 +52,7 @@
52
52
  "@vue/tsconfig": "^0.4.0",
53
53
  "eslint": "^8.51.0",
54
54
  "eslint-config-prettier": "^9.0.0",
55
- "eslint-plugin-prettier": "^5.0.0",
55
+ "eslint-plugin-prettier": "^5.0.1",
56
56
  "eslint-plugin-simple-import-sort": "^10.0.0",
57
57
  "eslint-plugin-vue": "^9.17.0",
58
58
  "husky": "^8.0.3",
@@ -62,6 +62,6 @@
62
62
  "typescript": "~5.2.2",
63
63
  "vite": "^4.4.11",
64
64
  "vitest": "^0.34.6",
65
- "vue-tsc": "^1.8.16"
65
+ "vue-tsc": "^1.8.19"
66
66
  }
67
67
  }
@@ -61,6 +61,13 @@ module Mihari
61
61
  Mihari.config.ignore_error
62
62
  end
63
63
 
64
+ #
65
+ # @return [Integer, nil]
66
+ #
67
+ def timeout
68
+ options[:timeout]
69
+ end
70
+
64
71
  # @return [Array<String>, Array<Mihari::Artifact>]
65
72
  def artifacts
66
73
  raise NotImplementedError, "You must implement #{self.class}##{__method__}"
@@ -97,7 +104,7 @@ module Mihari
97
104
  self.class.to_s.split("::").last
98
105
  end
99
106
 
100
- alias_method :source, :class_name
107
+ alias source class_name
101
108
 
102
109
  class << self
103
110
  #
@@ -32,7 +32,7 @@ module Mihari
32
32
  # @return [Mihari::Clients::BinaryEdge]
33
33
  #
34
34
  def client
35
- @client ||= Clients::BinaryEdge.new(api_key: api_key, interval: interval)
35
+ @client ||= Clients::BinaryEdge.new(api_key: api_key, interval: interval, timeout: timeout)
36
36
  end
37
37
  end
38
38
  end
@@ -52,7 +52,7 @@ module Mihari
52
52
  # @return [Mihari::Clients::Censys]
53
53
  #
54
54
  def client
55
- @client ||= Clients::Censys.new(id: id, secret: secret, interval: interval)
55
+ @client ||= Clients::Censys.new(id: id, secret: secret, interval: interval, timeout: timeout)
56
56
  end
57
57
 
58
58
  #
@@ -59,6 +59,7 @@ module Mihari
59
59
  query,
60
60
  method: method,
61
61
  headers: headers,
62
+ timeout: timeout,
62
63
  params: params,
63
64
  json: json,
64
65
  data: data
@@ -31,7 +31,7 @@ module Mihari
31
31
  private
32
32
 
33
33
  def client
34
- @client ||= Clients::GreyNoise.new(api_key: api_key, interval: interval)
34
+ @client ||= Clients::GreyNoise.new(api_key: api_key, interval: interval, timeout: timeout)
35
35
  end
36
36
  end
37
37
  end
@@ -46,7 +46,7 @@ module Mihari
46
46
  private
47
47
 
48
48
  def client
49
- @client ||= Clients::HunterHow.new(api_key: api_key, interval: interval)
49
+ @client ||= Clients::HunterHow.new(api_key: api_key, interval: interval, timeout: timeout)
50
50
  end
51
51
  end
52
52
  end
@@ -33,7 +33,7 @@ module Mihari
33
33
  private
34
34
 
35
35
  def client
36
- @client ||= Clients::Onyphe.new(api_key: api_key, interval: interval)
36
+ @client ||= Clients::Onyphe.new(api_key: api_key, interval: interval, timeout: timeout)
37
37
  end
38
38
  end
39
39
  end
@@ -34,7 +34,7 @@ module Mihari
34
34
  # @return [Clients::Shodan]
35
35
  #
36
36
  def client
37
- @client ||= Clients::Shodan.new(api_key: api_key, interval: interval)
37
+ @client ||= Clients::Shodan.new(api_key: api_key, interval: interval, timeout: timeout)
38
38
  end
39
39
  end
40
40
  end
@@ -44,7 +44,7 @@ module Mihari
44
44
  private
45
45
 
46
46
  def client
47
- @client ||= Clients::UrlScan.new(api_key: api_key, interval: interval)
47
+ @client ||= Clients::UrlScan.new(api_key: api_key, interval: interval, timeout: timeout)
48
48
  end
49
49
 
50
50
  #
@@ -33,7 +33,7 @@ module Mihari
33
33
  # @return [::VirusTotal::API]
34
34
  #
35
35
  def client
36
- @client = Clients::VirusTotal.new(api_key: api_key, interval: interval)
36
+ @client = Clients::VirusTotal.new(api_key: api_key, interval: interval, timeout: timeout)
37
37
  end
38
38
  end
39
39
  end
@@ -53,7 +53,7 @@ module Mihari
53
53
  end
54
54
 
55
55
  def client
56
- @client ||= Clients::ZoomEye.new(api_key: api_key, interval: interval)
56
+ @client ||= Clients::ZoomEye.new(api_key: api_key, interval: interval, timeout: timeout)
57
57
  end
58
58
 
59
59
  #
@@ -12,14 +12,20 @@ module Mihari
12
12
  # @return [Integer, nil]
13
13
  attr_reader :interval
14
14
 
15
+ # @return [Integer, nil]
16
+ attr_reader :timeout
17
+
15
18
  #
16
19
  # @param [String] base_url
17
20
  # @param [Hash] headers
21
+ # @param [Integer, nil] interval
22
+ # @param [Integer, nil] timeout
18
23
  #
19
- def initialize(base_url, headers: {}, interval: nil)
24
+ def initialize(base_url, headers: {}, interval: nil, timeout: nil)
20
25
  @base_url = base_url
21
26
  @headers = headers || {}
22
27
  @interval = interval
28
+ @timeout = timeout
23
29
  end
24
30
 
25
31
  private
@@ -28,6 +34,13 @@ module Mihari
28
34
  sleep(interval) if interval
29
35
  end
30
36
 
37
+ #
38
+ # @return [::HTTP::Client]
39
+ #
40
+ def http
41
+ HTTP::Factory.build headers: headers, timeout: timeout
42
+ end
43
+
31
44
  #
32
45
  # @param [String] path
33
46
  #
@@ -41,20 +54,20 @@ module Mihari
41
54
  # @param [String] path
42
55
  # @param [Hash, nil] params
43
56
  #
44
- # @return [String] <description>
57
+ # @return [::HTTP::Response]
45
58
  #
46
59
  def get(path, params: nil)
47
- HTTP.get(url_for(path), headers: headers, params: params)
60
+ http.get(url_for(path), params: params)
48
61
  end
49
62
 
50
63
  #
51
64
  # @param [String] path
52
65
  # @param [Hash, nil] json
53
66
  #
54
- # @return [String] <description>
67
+ # @return [::HTTP::Response]
55
68
  #
56
69
  def post(path, json: {})
57
- HTTP.post(url_for(path), headers: headers, json: json)
70
+ http.post(url_for(path), json: json)
58
71
  end
59
72
  end
60
73
  end
@@ -7,14 +7,15 @@ module Mihari
7
7
  # @param [String] base_url
8
8
  # @param [String, nil] api_key
9
9
  # @param [Hash] headers
10
- # @param [Integer.nil ] interval
10
+ # @param [Integer, nil] interval
11
+ # @param [Integer, nil] timeout
11
12
  #
12
- def initialize(base_url = "https://api.binaryedge.io/v2", api_key:, headers: {}, interval: nil)
13
+ def initialize(base_url = "https://api.binaryedge.io/v2", api_key:, headers: {}, interval: nil, timeout: nil)
13
14
  raise(ArgumentError, "'api_key' argument is required") unless api_key
14
15
 
15
16
  headers["x-key"] = api_key
16
17
 
17
- super(base_url, headers: headers, interval: interval)
18
+ super(base_url, headers: headers, interval: interval, timeout: timeout)
18
19
  end
19
20
 
20
21
  #
@@ -11,14 +11,15 @@ module Mihari
11
11
  # @param [String, nil] secret
12
12
  # @param [Hash] headers
13
13
  # @param [Integer, nil] interval
14
+ # @param [Integer, nil] timeout
14
15
  #
15
- def initialize(base_url = "https://search.censys.io", id:, secret:, headers: {}, interval: nil)
16
+ def initialize(base_url = "https://search.censys.io", id:, secret:, headers: {}, interval: nil, timeout: nil)
16
17
  raise(ArgumentError, "'id' argument is required") if id.nil?
17
18
  raise(ArgumentError, "'secret' argument is required") if secret.nil?
18
19
 
19
20
  headers["authorization"] = "Basic #{Base64.strict_encode64("#{id}:#{secret}")}"
20
21
 
21
- super(base_url, headers: headers, interval: interval)
22
+ super(base_url, headers: headers, interval: interval, timeout: timeout)
22
23
  end
23
24
 
24
25
  #
@@ -10,12 +10,13 @@ module Mihari
10
10
  # @param [String, nil] api_key
11
11
  # @param [Hash] headers
12
12
  # @param [Integer, nil] interval
13
+ # @param [Integer, nil] timeout
13
14
  #
14
- def initialize(base_url = "https://api.greynoise.io", api_key:, headers: {}, interval: nil)
15
+ def initialize(base_url = "https://api.greynoise.io", api_key:, headers: {}, interval: nil, timeout: nil)
15
16
  raise(ArgumentError, "'api_key' argument is required") unless api_key
16
17
 
17
18
  headers["key"] = api_key
18
- super(base_url, headers: headers, interval: interval)
19
+ super(base_url, headers: headers, interval: interval, timeout: timeout)
19
20
  end
20
21
 
21
22
  #
@@ -15,11 +15,12 @@ module Mihari
15
15
  # @param [String, nil] api_key
16
16
  # @param [Hash] headers
17
17
  # @param [Integer, nil] interval
18
+ # @param [Integer, nil] timeout
18
19
  #
19
- def initialize(base_url = "https://api.hunter.how/", api_key:, headers: {}, interval: nil)
20
+ def initialize(base_url = "https://api.hunter.how/", api_key:, headers: {}, interval: nil, timeout: nil)
20
21
  raise(ArgumentError, "'api_key' argument is required") unless api_key
21
22
 
22
- super(base_url, headers: headers, interval: interval)
23
+ super(base_url, headers: headers, interval: interval, timeout: timeout)
23
24
 
24
25
  @api_key = api_key
25
26
  end
@@ -12,11 +12,13 @@ module Mihari
12
12
  # @param [String] base_url
13
13
  # @param [String, nil] api_key
14
14
  # @param [Hash] headers
15
+ # @param [Integer, nil] interval
16
+ # @param [Integer, nil] timeout
15
17
  #
16
- def initialize(base_url = "https://www.onyphe.io", api_key:, headers: {}, interval: nil)
18
+ def initialize(base_url = "https://www.onyphe.io", api_key:, headers: {}, interval: nil, timeout: nil)
17
19
  raise(ArgumentError, "'api_key' argument is required") if api_key.nil?
18
20
 
19
- super(base_url, headers: headers, interval: interval)
21
+ super(base_url, headers: headers, interval: interval, timeout: timeout)
20
22
 
21
23
  @api_key = api_key
22
24
  end
@@ -13,11 +13,12 @@ module Mihari
13
13
  # @param [String, nil] api_key
14
14
  # @param [Hash] headers
15
15
  # @param [Integer, nil] interval
16
+ # @param [Integer, nil] timeout
16
17
  #
17
- def initialize(base_url = "https://api.shodan.io", api_key:, headers: {}, interval: nil)
18
+ def initialize(base_url = "https://api.shodan.io", api_key:, headers: {}, interval: nil, timeout: nil)
18
19
  raise(ArgumentError, "'api_key' argument is required") unless api_key
19
20
 
20
- super(base_url, headers: headers, interval: interval)
21
+ super(base_url, headers: headers, interval: interval, timeout: timeout)
21
22
 
22
23
  @api_key = api_key
23
24
  end
@@ -8,13 +8,14 @@ module Mihari
8
8
  # @param [String, nil] api_key
9
9
  # @param [Hash] headers
10
10
  # @param [Interval, nil] interval
11
+ # @param [Interval, nil] timeout
11
12
  #
12
- def initialize(base_url = "https://urlscan.io", api_key:, headers: {}, interval: nil)
13
+ def initialize(base_url = "https://urlscan.io", api_key:, headers: {}, interval: nil, timeout: nil)
13
14
  raise(ArgumentError, "'api_key' argument is required") if api_key.nil?
14
15
 
15
16
  headers["api-key"] = api_key
16
17
 
17
- super(base_url, headers: headers, interval: interval)
18
+ super(base_url, headers: headers, interval: interval, timeout: timeout)
18
19
  end
19
20
 
20
21
  #
@@ -8,13 +8,14 @@ module Mihari
8
8
  # @param [String, nil] api_key
9
9
  # @param [Hash] headers
10
10
  # @param [Integer, nil] interval
11
+ # @param [Integer, nil] timeout
11
12
  #
12
- def initialize(base_url = "https://www.virustotal.com", api_key:, headers: {}, interval: nil)
13
+ def initialize(base_url = "https://www.virustotal.com", api_key:, headers: {}, interval: nil, timeout: nil)
13
14
  raise(ArgumentError, "'api_key' argument is required") if api_key.nil?
14
15
 
15
16
  headers["x-apikey"] = api_key
16
17
 
17
- super(base_url, headers: headers, interval: interval)
18
+ super(base_url, headers: headers, interval: interval, timeout: timeout)
18
19
  end
19
20
 
20
21
  #
@@ -12,12 +12,13 @@ module Mihari
12
12
  # @param [String, nil] api_key
13
13
  # @param [Hash] headers
14
14
  # @param [Integer, nil] interval
15
+ # @param [Integer, nil] timeout
15
16
  #
16
- def initialize(base_url = "https://api.zoomeye.org", api_key:, headers: {}, interval: nil)
17
+ def initialize(base_url = "https://api.zoomeye.org", api_key:, headers: {}, interval: nil, timeout: nil)
17
18
  raise(ArgumentError, "'api_key' argument is required") unless api_key
18
19
 
19
20
  headers["api-key"] = api_key
20
- super(base_url, headers: headers, interval: interval)
21
+ super(base_url, headers: headers, interval: interval, timeout: timeout)
21
22
  end
22
23
 
23
24
  #
data/lib/mihari/config.rb CHANGED
@@ -97,6 +97,10 @@ module Mihari
97
97
  attr_reader :ignore_error
98
98
 
99
99
  def initialize
100
+ load
101
+ end
102
+
103
+ def load
100
104
  @binaryedge_api_key = ENV.fetch("BINARYEDGE_API_KEY", nil)
101
105
 
102
106
  @censys_id = ENV.fetch("CENSYS_ID", nil)
@@ -153,5 +157,14 @@ module Mihari
153
157
 
154
158
  @ignore_error = ENV.fetch("IGNORE_ERROR", false)
155
159
  end
160
+
161
+ #
162
+ # @return [Array<String>]
163
+ #
164
+ def keys
165
+ instance_variables.map do |key|
166
+ key[1..].to_s.upcase
167
+ end
168
+ end
156
169
  end
157
170
  end
@@ -72,17 +72,13 @@ module Mihari
72
72
  def emit
73
73
  return if artifacts.empty?
74
74
 
75
- client = Mihari::HTTP.new(url, headers: headers)
76
-
77
- res = nil
75
+ # returns body to prevent Parallel issue (Parallel fails to handle HTTP:Response object)
78
76
  case method
79
77
  when "GET"
80
- res = client.get
78
+ http.get(url).body.to_s
81
79
  when "POST"
82
- res = client.post(json: payload)
80
+ http.post(url, json: json).body.to_s
83
81
  end
84
-
85
- res
86
82
  end
87
83
 
88
84
  def valid?
@@ -93,12 +89,16 @@ module Mihari
93
89
 
94
90
  private
95
91
 
92
+ def http
93
+ HTTP::Factory.build headers: headers
94
+ end
95
+
96
96
  #
97
- # Convert payload into string
97
+ # Render template
98
98
  #
99
99
  # @return [String]
100
100
  #
101
- def payload_as_string
101
+ def rendered_template
102
102
  [].tap do |out|
103
103
  options = {}
104
104
  options[:template] = File.read(template) unless template.nil?
@@ -115,8 +115,8 @@ module Mihari
115
115
  #
116
116
  # @return [Hash]
117
117
  #
118
- def payload
119
- JSON.parse payload_as_string
118
+ def json
119
+ JSON.parse rendered_template
120
120
  end
121
121
  end
122
122
  end
@@ -37,7 +37,7 @@ module Mihari
37
37
  def query_by_type(name, resource_type)
38
38
  url = "https://dns.google/resolve"
39
39
  params = { name: name, type: resource_type }
40
- res = HTTP.get(url, params: params)
40
+ res = http.get(url, params: params)
41
41
 
42
42
  data = JSON.parse(res.body.to_s)
43
43
 
@@ -45,6 +45,12 @@ module Mihari
45
45
  rescue HTTPError
46
46
  nil
47
47
  end
48
+
49
+ private
50
+
51
+ def http
52
+ HTTP::Factory.build
53
+ end
48
54
  end
49
55
  end
50
56
  end
@@ -28,18 +28,25 @@ module Mihari
28
28
  # @return [Mihari::Structs::IPInfo::Response, nil]
29
29
  #
30
30
  def query(ip)
31
- headers = {}
32
-
33
- token = Mihari.config.ipinfo_api_key
34
- headers[:authorization] = "Bearer #{token}" unless token.nil?
35
-
36
31
  url = "https://ipinfo.io/#{ip}/json"
37
- res = HTTP.get(url, headers: headers)
32
+ res = http.get(url)
38
33
  data = JSON.parse(res.body.to_s)
39
34
 
40
35
  Structs::IPInfo::Response.from_dynamic! data
41
36
  end
42
37
  memoize :query
38
+
39
+ private
40
+
41
+ def headers
42
+ token = Mihari.config.ipinfo_api_key
43
+ authorization = token.nil? ? nil : "Bearer #{token}"
44
+ { authorization: authorization }.compact
45
+ end
46
+
47
+ def http
48
+ HTTP::Factory.build headers: headers
49
+ end
43
50
  end
44
51
  end
45
52
  end
@@ -23,12 +23,18 @@ module Mihari
23
23
  #
24
24
  def query(ip)
25
25
  url = "https://internetdb.shodan.io/#{ip}"
26
- res = HTTP.get(url)
26
+ res = http.get(url)
27
27
  data = JSON.parse(res.body.to_s)
28
28
 
29
29
  Structs::Shodan::InternetDBResponse.from_dynamic! data
30
30
  end
31
31
  memoize :query
32
+
33
+ private
34
+
35
+ def http
36
+ HTTP::Factory.build
37
+ end
32
38
  end
33
39
  end
34
40
  end
data/lib/mihari/errors.rb CHANGED
@@ -20,8 +20,6 @@ module Mihari
20
20
 
21
21
  class TimeoutError < HTTPError; end
22
22
 
23
- class SSLError < HTTPError; end
24
-
25
23
  class StatusCodeError < HTTPError
26
24
  # @return [Integer]
27
25
  attr_reader :status_code
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "csv"
4
- require "insensitive_hash"
5
4
 
6
5
  module Mihari
7
6
  module Feed
@@ -24,16 +23,18 @@ module Mihari
24
23
  # @return [String]
25
24
  attr_reader :method
26
25
 
27
- def initialize(url, headers: {}, method: "GET", params: nil, json: nil, data: nil)
26
+ # @return [Integer, nil]
27
+ attr_reader :timeout
28
+
29
+ def initialize(url, headers: {}, method: "GET", params: nil, json: nil, data: nil, timeout: nil)
28
30
  @url = Addressable::URI.parse(url)
29
- @headers = headers.insensitive
31
+ @headers = headers
30
32
  @method = method
33
+ @timeout = timeout
31
34
 
32
35
  @params = params
33
36
  @json = json
34
37
  @data = data
35
-
36
- headers["content-type"] = "application/json" unless json.nil?
37
38
  end
38
39
 
39
40
  #
@@ -43,14 +44,12 @@ module Mihari
43
44
  return read_file(url.path) if url.scheme == "file"
44
45
 
45
46
  res = nil
46
- client = HTTP.new(url, headers: headers)
47
-
48
- res = client.get(params: params) if method == "GET"
49
- res = client.post(params: params, json: json, data: data) if method == "POST"
47
+ res = http.get(url, params: params) if method == "GET"
48
+ res = http.post(url, params: params, json: json, form: data) if method == "POST"
50
49
 
51
50
  return [] if res.nil?
52
51
 
53
- body = res.body
52
+ body = res.body.to_s
54
53
  content_type = res["Content-Type"].to_s
55
54
  return convert_as_json(body) if content_type.include?("application/json")
56
55
 
@@ -98,6 +97,12 @@ module Mihari
98
97
 
99
98
  convert_as_csv text
100
99
  end
100
+
101
+ private
102
+
103
+ def http
104
+ HTTP::Factory.build headers: headers, timeout: timeout
105
+ end
101
106
  end
102
107
  end
103
108
  end