mihari 5.4.9 → 5.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/docs/analyzers/binaryedge.md +2 -2
- data/docs/analyzers/censys.md +3 -3
- data/docs/analyzers/circl.md +3 -3
- data/docs/analyzers/crtsh.md +2 -2
- data/docs/analyzers/dnstwister.md +1 -1
- data/docs/analyzers/feed.md +7 -7
- data/docs/analyzers/greynoise.md +2 -2
- data/docs/analyzers/hunterhow.md +4 -4
- data/docs/analyzers/index.md +13 -8
- data/docs/analyzers/onyphe.md +2 -2
- data/docs/analyzers/otx.md +2 -2
- data/docs/analyzers/passivetotal.md +7 -3
- data/docs/analyzers/pulsedive.md +2 -2
- data/docs/analyzers/securitytrails.md +6 -2
- data/docs/analyzers/shodan.md +2 -2
- data/docs/analyzers/urlscan.md +2 -2
- data/docs/analyzers/virustotal.md +6 -2
- data/docs/analyzers/virustotal_intelligence.md +6 -2
- data/docs/analyzers/zoomeye.md +3 -3
- data/docs/emitters/hive.md +4 -4
- data/docs/emitters/index.md +29 -0
- data/docs/emitters/misp.md +2 -2
- data/docs/emitters/slack.md +2 -7
- data/docs/emitters/webhook.md +4 -4
- data/docs/enrichers/index.md +29 -0
- data/docs/enrichers/ipinfo.md +7 -0
- data/docs/index.md +0 -2
- data/docs/installation.md +1 -1
- data/docs/rule.md +12 -15
- data/docs/usage.md +5 -2
- data/frontend/package-lock.json +294 -2772
- data/frontend/package.json +10 -10
- data/frontend/src/components/ErrorMessage.vue +0 -1
- data/frontend/src/components/alert/Alerts.vue +0 -1
- data/frontend/src/components/alert/AlertsWithPagination.vue +0 -1
- data/frontend/src/components/alert/AlertsWrapper.vue +0 -6
- data/frontend/src/components/alert/Form.vue +1 -3
- data/frontend/src/components/artifact/Artifact.vue +0 -17
- data/frontend/src/components/artifact/ArtifactWrapper.vue +0 -2
- data/frontend/src/components/artifact/WhoisRecord.vue +0 -3
- data/frontend/src/components/config/ConfigsWrapper.vue +0 -2
- data/frontend/src/components/rule/EditRule.vue +0 -3
- data/frontend/src/components/rule/EditRuleWrapper.vue +0 -2
- data/frontend/src/components/rule/Form.vue +1 -3
- data/frontend/src/components/rule/NewRule.vue +0 -3
- data/frontend/src/components/rule/Rule.vue +1 -7
- data/frontend/src/components/rule/RuleWrapper.vue +0 -2
- data/frontend/src/components/rule/RulesWrapper.vue +0 -6
- data/frontend/src/swagger.yaml +254 -254
- data/lib/mihari/analyzers/base.rb +7 -37
- data/lib/mihari/analyzers/binaryedge.rb +5 -1
- data/lib/mihari/analyzers/censys.rb +6 -1
- data/lib/mihari/analyzers/greynoise.rb +5 -1
- data/lib/mihari/analyzers/hunterhow.rb +5 -1
- data/lib/mihari/analyzers/onyphe.rb +5 -1
- data/lib/mihari/analyzers/passivetotal.rb +9 -0
- data/lib/mihari/analyzers/pulsedive.rb +1 -1
- data/lib/mihari/analyzers/rule.rb +55 -54
- data/lib/mihari/analyzers/securitytrails.rb +9 -0
- data/lib/mihari/analyzers/shodan.rb +5 -1
- data/lib/mihari/analyzers/urlscan.rb +5 -1
- data/lib/mihari/analyzers/virustotal.rb +11 -2
- data/lib/mihari/analyzers/virustotal_intelligence.rb +21 -1
- data/lib/mihari/analyzers/zoomeye.rb +7 -3
- data/lib/mihari/base.rb +69 -0
- data/lib/mihari/cli/main.rb +36 -0
- data/lib/mihari/clients/base.rb +7 -7
- data/lib/mihari/clients/binaryedge.rb +10 -4
- data/lib/mihari/clients/censys.rb +11 -4
- data/lib/mihari/clients/greynoise.rb +10 -4
- data/lib/mihari/clients/hunterhow.rb +10 -4
- data/lib/mihari/clients/misp.rb +3 -2
- data/lib/mihari/clients/onyphe.rb +10 -4
- data/lib/mihari/clients/shodan.rb +10 -4
- data/lib/mihari/clients/the_hive.rb +3 -2
- data/lib/mihari/clients/urlscan.rb +9 -3
- data/lib/mihari/clients/virustotal.rb +10 -4
- data/lib/mihari/clients/zoomeye.rb +11 -5
- data/lib/mihari/commands/alert.rb +6 -33
- data/lib/mihari/commands/rule.rb +7 -12
- data/lib/mihari/commands/search.rb +10 -38
- data/lib/mihari/config.rb +8 -0
- data/lib/mihari/constants.rb +3 -3
- data/lib/mihari/emitters/base.rb +22 -15
- data/lib/mihari/emitters/database.rb +1 -1
- data/lib/mihari/emitters/misp.rb +7 -6
- data/lib/mihari/emitters/slack.rb +24 -6
- data/lib/mihari/emitters/the_hive.rb +8 -7
- data/lib/mihari/emitters/webhook.rb +31 -29
- data/lib/mihari/enrichers/base.rb +25 -19
- data/lib/mihari/enrichers/google_public_dns.rb +38 -38
- data/lib/mihari/enrichers/ipinfo.rb +32 -34
- data/lib/mihari/enrichers/shodan.rb +18 -26
- data/lib/mihari/enrichers/whois.rb +121 -111
- data/lib/mihari/mixins/retriable.rb +4 -2
- data/lib/mihari/models/artifact.rb +37 -23
- data/lib/mihari/models/autonomous_system.rb +3 -2
- data/lib/mihari/models/cpe.rb +3 -2
- data/lib/mihari/models/dns.rb +3 -2
- data/lib/mihari/models/geolocation.rb +3 -2
- data/lib/mihari/models/port.rb +3 -2
- data/lib/mihari/models/reverse_dns.rb +3 -2
- data/lib/mihari/models/whois.rb +4 -3
- data/lib/mihari/schemas/analyzer.rb +24 -23
- data/lib/mihari/schemas/emitter.rb +32 -25
- data/lib/mihari/schemas/enricher.rb +21 -2
- data/lib/mihari/schemas/options.rb +27 -0
- data/lib/mihari/schemas/rule.rb +8 -4
- data/lib/mihari/services/alert_runner.rb +1 -1
- data/lib/mihari/services/rule_runner.rb +1 -11
- data/lib/mihari/types.rb +1 -14
- data/lib/mihari/version.rb +1 -1
- data/lib/mihari/web/endpoints/ip_addresses.rb +1 -1
- data/lib/mihari/web/public/assets/{index-33165282.css → index-56fc2187.css} +1 -1
- data/lib/mihari/web/public/assets/index-9cc489e6.js +1749 -0
- data/lib/mihari/web/public/index.html +2 -2
- data/lib/mihari/web/public/redoc-static.html +400 -400
- data/lib/mihari.rb +67 -37
- data/mihari.gemspec +3 -2
- data/mkdocs.yml +8 -6
- data/requirements.txt +1 -1
- metadata +24 -8
- data/lib/mihari/web/public/assets/index-a92abd57.js +0 -1740
data/lib/mihari/emitters/misp.rb
CHANGED
@@ -18,13 +18,14 @@ module Mihari
|
|
18
18
|
#
|
19
19
|
# @param [Array<Mihari::Artifact>] artifacts
|
20
20
|
# @param [Mihari::Services::Rule] rule
|
21
|
-
# @param [Hash]
|
21
|
+
# @param [Hash, nil] options
|
22
|
+
# @param [Hash] **params
|
22
23
|
#
|
23
|
-
def initialize(artifacts:, rule:, **
|
24
|
-
super(artifacts: artifacts, rule: rule,
|
24
|
+
def initialize(artifacts:, rule:, options: nil, **params)
|
25
|
+
super(artifacts: artifacts, rule: rule, options: options)
|
25
26
|
|
26
|
-
@url =
|
27
|
-
@api_key =
|
27
|
+
@url = params[:url] || Mihari.config.misp_url
|
28
|
+
@api_key = params[:api_key] || Mihari.config.misp_api_key
|
28
29
|
end
|
29
30
|
|
30
31
|
# @return [Boolean]
|
@@ -70,7 +71,7 @@ module Mihari
|
|
70
71
|
end
|
71
72
|
|
72
73
|
def client
|
73
|
-
@client ||= Clients::MISP.new(url, api_key: api_key)
|
74
|
+
@client ||= Clients::MISP.new(url, api_key: api_key, timeout: timeout)
|
74
75
|
end
|
75
76
|
|
76
77
|
#
|
@@ -134,13 +134,14 @@ module Mihari
|
|
134
134
|
#
|
135
135
|
# @param [Array<Mihari::Artifact>] artifacts
|
136
136
|
# @param [Mihari::Services::Rule] rule
|
137
|
-
# @param [Hash]
|
137
|
+
# @param [Hash, nil] options
|
138
|
+
# @param [Hash] **params
|
138
139
|
#
|
139
|
-
def initialize(artifacts:, rule:, **
|
140
|
-
super(artifacts: artifacts, rule: rule,
|
140
|
+
def initialize(artifacts:, rule:, options: nil, **params)
|
141
|
+
super(artifacts: artifacts, rule: rule, options: options)
|
141
142
|
|
142
|
-
@webhook_url =
|
143
|
-
@channel =
|
143
|
+
@webhook_url = params[:webhook_url] || Mihari.config.slack_webhook_url
|
144
|
+
@channel = params[:channel] || Mihari.config.slack_channel || DEFAULT_CHANNEL
|
144
145
|
@username = DEFAULT_USERNAME
|
145
146
|
end
|
146
147
|
|
@@ -162,8 +163,25 @@ module Mihari
|
|
162
163
|
webhook_url?
|
163
164
|
end
|
164
165
|
|
166
|
+
#
|
167
|
+
# @return [::Slack::Notifier]
|
168
|
+
#
|
165
169
|
def notifier
|
166
|
-
@notifier ||=
|
170
|
+
@notifier ||= [].tap do |out|
|
171
|
+
out << if timeout.nil?
|
172
|
+
::Slack::Notifier.new(
|
173
|
+
webhook_url,
|
174
|
+
channel: channel, username: username
|
175
|
+
)
|
176
|
+
else
|
177
|
+
::Slack::Notifier.new(
|
178
|
+
webhook_url,
|
179
|
+
channel: channel,
|
180
|
+
username: username,
|
181
|
+
http_options: { timeout: timeout }
|
182
|
+
)
|
183
|
+
end
|
184
|
+
end.first
|
167
185
|
end
|
168
186
|
|
169
187
|
#
|
@@ -15,14 +15,15 @@ module Mihari
|
|
15
15
|
#
|
16
16
|
# @param [Array<Mihari::Artifact>] artifacts
|
17
17
|
# @param [Mihari::Services::Rule] rule
|
18
|
-
# @param [Hash]
|
18
|
+
# @param [Hash, nil] options
|
19
|
+
# @param [Hash] **params
|
19
20
|
#
|
20
|
-
def initialize(artifacts:, rule:, **
|
21
|
-
super(artifacts: artifacts, rule: rule,
|
21
|
+
def initialize(artifacts:, rule:, options: nil, **params)
|
22
|
+
super(artifacts: artifacts, rule: rule, options: options)
|
22
23
|
|
23
|
-
@url =
|
24
|
-
@api_key =
|
25
|
-
@api_version =
|
24
|
+
@url = params[:url] || Mihari.config.thehive_url
|
25
|
+
@api_key = params[:api_key] || Mihari.config.thehive_api_key
|
26
|
+
@api_version = params[:api_version] || Mihari.config.thehive_api_version
|
26
27
|
end
|
27
28
|
|
28
29
|
# @return [Boolean]
|
@@ -79,7 +80,7 @@ module Mihari
|
|
79
80
|
end
|
80
81
|
|
81
82
|
def client
|
82
|
-
@client ||= Clients::TheHive.new(url, api_key: api_key, api_version: normalized_api_version)
|
83
|
+
@client ||= Clients::TheHive.new(url, api_key: api_key, api_version: normalized_api_version, timeout: timeout)
|
83
84
|
end
|
84
85
|
|
85
86
|
#
|
@@ -5,28 +5,30 @@ require "erb"
|
|
5
5
|
module Mihari
|
6
6
|
module Emitters
|
7
7
|
class PayloadTemplate < ERB
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
"
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
8
|
+
class << self
|
9
|
+
def template
|
10
|
+
%{
|
11
|
+
{
|
12
|
+
"rule": {
|
13
|
+
"id": "<%= @rule.id %>",
|
14
|
+
"title": "<%= @rule.title %>",
|
15
|
+
"description": "<%= @rule.description %>"
|
16
|
+
},
|
17
|
+
"artifacts": [
|
18
|
+
<% @artifacts.each_with_index do |artifact, idx| %>
|
19
|
+
"<%= artifact.data %>"
|
20
|
+
<%= ',' if idx < (@artifacts.length - 1) %>
|
21
|
+
<% end %>
|
22
|
+
],
|
23
|
+
"tags": [
|
24
|
+
<% @rule.tags.each_with_index do |tag, idx| %>
|
25
|
+
"<%= tag %>"
|
26
|
+
<%= ',' if idx < (@rule.tags.length - 1) %>
|
27
|
+
<% end %>
|
28
|
+
]
|
29
|
+
}
|
30
|
+
}
|
31
|
+
end
|
30
32
|
end
|
31
33
|
|
32
34
|
def initialize(artifacts:, rule:, options: {})
|
@@ -60,13 +62,13 @@ module Mihari
|
|
60
62
|
# @param [Mihari::Services::Rule] rule
|
61
63
|
# @param [Hash] **options
|
62
64
|
#
|
63
|
-
def initialize(artifacts:, rule:, **
|
64
|
-
super(artifacts: artifacts, rule: rule,
|
65
|
+
def initialize(artifacts:, rule:, options: nil, **params)
|
66
|
+
super(artifacts: artifacts, rule: rule, options: options)
|
65
67
|
|
66
|
-
@url = Addressable::URI.parse(
|
67
|
-
@headers =
|
68
|
-
@method =
|
69
|
-
@template =
|
68
|
+
@url = Addressable::URI.parse(params[:url])
|
69
|
+
@headers = params[:headers] || {}
|
70
|
+
@method = params[:method] || "POST"
|
71
|
+
@template = params[:template]
|
70
72
|
end
|
71
73
|
|
72
74
|
def emit
|
@@ -90,7 +92,7 @@ module Mihari
|
|
90
92
|
private
|
91
93
|
|
92
94
|
def http
|
93
|
-
HTTP::Factory.build headers: headers
|
95
|
+
HTTP::Factory.build headers: headers, timeout: timeout
|
94
96
|
end
|
95
97
|
|
96
98
|
#
|
@@ -2,33 +2,39 @@
|
|
2
2
|
|
3
3
|
module Mihari
|
4
4
|
module Enrichers
|
5
|
-
class Base
|
5
|
+
class Base < Mihari::Base
|
6
6
|
include Mixins::Configurable
|
7
|
+
include Mixins::Retriable
|
7
8
|
|
8
|
-
|
9
|
-
include Dry::Monads[:result, :try]
|
10
|
-
|
11
|
-
def inherited(child)
|
12
|
-
super
|
13
|
-
Mihari.enrichers << child
|
14
|
-
end
|
9
|
+
include Dry::Monads[:result, :try]
|
15
10
|
|
16
|
-
|
17
|
-
|
18
|
-
|
11
|
+
def initialize(options: nil)
|
12
|
+
super(options: options)
|
13
|
+
end
|
19
14
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
15
|
+
def query_result(value)
|
16
|
+
Try[StandardError] do
|
17
|
+
retry_on_error(
|
18
|
+
times: retry_times,
|
19
|
+
interval: retry_interval,
|
20
|
+
exponential_backoff: retry_exponential_backoff
|
21
|
+
) { query value }
|
22
|
+
end.to_result
|
26
23
|
end
|
27
24
|
|
28
|
-
#
|
29
|
-
|
25
|
+
#
|
26
|
+
# @param [String] value
|
27
|
+
#
|
28
|
+
def query(value)
|
30
29
|
raise NotImplementedError, "You must implement #{self.class}##{__method__}"
|
31
30
|
end
|
31
|
+
|
32
|
+
class << self
|
33
|
+
def inherited(child)
|
34
|
+
super
|
35
|
+
Mihari.enrichers << child
|
36
|
+
end
|
37
|
+
end
|
32
38
|
end
|
33
39
|
end
|
34
40
|
end
|
@@ -5,52 +5,52 @@ require "net/https"
|
|
5
5
|
module Mihari
|
6
6
|
module Enrichers
|
7
7
|
class GooglePublicDNS < Base
|
8
|
-
#
|
9
|
-
|
10
|
-
|
8
|
+
#
|
9
|
+
# Query Google Public DNS
|
10
|
+
#
|
11
|
+
# @param [String] name
|
12
|
+
#
|
13
|
+
# @return [Array<Mihari::Structs::Shodan::GooglePublicDNS::Response>]
|
14
|
+
#
|
15
|
+
def query(name)
|
16
|
+
%w[A AAAA CNAME TXT NS].filter_map do |resource_type|
|
17
|
+
query_by_type(name, resource_type)
|
18
|
+
end
|
11
19
|
end
|
12
20
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
21
|
+
#
|
22
|
+
# Query Google Public DNS by resource type
|
23
|
+
#
|
24
|
+
# @param [String] name
|
25
|
+
# @param [String] resource_type
|
26
|
+
#
|
27
|
+
# @return [Mihari::Structs::Shodan::GooglePublicDNS::Response, nil]
|
28
|
+
#
|
29
|
+
def query_by_type(name, resource_type)
|
30
|
+
url = "https://dns.google/resolve"
|
31
|
+
params = { name: name, type: resource_type }
|
32
|
+
res = http.get(url, params: params)
|
33
|
+
|
34
|
+
data = JSON.parse(res.body.to_s)
|
35
|
+
|
36
|
+
Structs::GooglePublicDNS::Response.from_dynamic! data
|
37
|
+
rescue HTTPError
|
38
|
+
nil
|
39
|
+
end
|
28
40
|
|
41
|
+
class << self
|
29
42
|
#
|
30
|
-
#
|
31
|
-
#
|
32
|
-
# @param [String] name
|
33
|
-
# @param [String] resource_type
|
34
|
-
#
|
35
|
-
# @return [Mihari::Structs::Shodan::GooglePublicDNS::Response, nil]
|
43
|
+
# @return [String]
|
36
44
|
#
|
37
|
-
def
|
38
|
-
|
39
|
-
params = { name: name, type: resource_type }
|
40
|
-
res = http.get(url, params: params)
|
41
|
-
|
42
|
-
data = JSON.parse(res.body.to_s)
|
43
|
-
|
44
|
-
Structs::GooglePublicDNS::Response.from_dynamic! data
|
45
|
-
rescue HTTPError
|
46
|
-
nil
|
45
|
+
def class_key
|
46
|
+
"google_public_dns"
|
47
47
|
end
|
48
|
+
end
|
48
49
|
|
49
|
-
|
50
|
+
private
|
50
51
|
|
51
|
-
|
52
|
-
|
53
|
-
end
|
52
|
+
def http
|
53
|
+
HTTP::Factory.build timeout: timeout
|
54
54
|
end
|
55
55
|
end
|
56
56
|
end
|
@@ -5,9 +5,15 @@ require "net/https"
|
|
5
5
|
module Mihari
|
6
6
|
module Enrichers
|
7
7
|
class IPInfo < Base
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
include Memist::Memoizable
|
9
|
+
|
10
|
+
# @return [String, nil]
|
11
|
+
attr_reader :api_key
|
12
|
+
|
13
|
+
def initialize(options: nil, api_key: nil)
|
14
|
+
@api_key = api_key || Mihari.config.ipinfo_api_key
|
15
|
+
|
16
|
+
super(options: options)
|
11
17
|
end
|
12
18
|
|
13
19
|
private
|
@@ -16,37 +22,29 @@ module Mihari
|
|
16
22
|
%w[ipinfo_api_key]
|
17
23
|
end
|
18
24
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
25
|
+
#
|
26
|
+
# Query IPInfo
|
27
|
+
#
|
28
|
+
# @param [String] ip
|
29
|
+
#
|
30
|
+
# @return [Mihari::Structs::IPInfo::Response, nil]
|
31
|
+
#
|
32
|
+
def query(ip)
|
33
|
+
url = "https://ipinfo.io/#{ip}/json"
|
34
|
+
res = http.get(url)
|
35
|
+
data = JSON.parse(res.body.to_s)
|
36
|
+
|
37
|
+
Structs::IPInfo::Response.from_dynamic! data
|
38
|
+
end
|
39
|
+
memoize :query
|
40
|
+
|
41
|
+
def headers
|
42
|
+
authorization = api_key.nil? ? nil : "Bearer #{api_key}"
|
43
|
+
{ authorization: authorization }.compact
|
44
|
+
end
|
45
|
+
|
46
|
+
def http
|
47
|
+
HTTP::Factory.build headers: headers, timeout: timeout
|
50
48
|
end
|
51
49
|
end
|
52
50
|
end
|
@@ -5,36 +5,28 @@ require "net/https"
|
|
5
5
|
module Mihari
|
6
6
|
module Enrichers
|
7
7
|
class Shodan < Base
|
8
|
-
|
9
|
-
def valid?
|
10
|
-
true
|
11
|
-
end
|
12
|
-
|
13
|
-
class << self
|
14
|
-
include Dry::Monads[:result]
|
15
|
-
include Memist::Memoizable
|
8
|
+
include Memist::Memoizable
|
16
9
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
10
|
+
#
|
11
|
+
# Query Shodan Internet DB
|
12
|
+
#
|
13
|
+
# @param [String] ip
|
14
|
+
#
|
15
|
+
# @return [Mihari::Structs::Shodan::InternetDBResponse, nil]
|
16
|
+
#
|
17
|
+
def query(ip)
|
18
|
+
url = "https://internetdb.shodan.io/#{ip}"
|
19
|
+
res = http.get(url)
|
20
|
+
data = JSON.parse(res.body.to_s)
|
28
21
|
|
29
|
-
|
30
|
-
|
31
|
-
|
22
|
+
Structs::Shodan::InternetDBResponse.from_dynamic! data
|
23
|
+
end
|
24
|
+
memoize :query
|
32
25
|
|
33
|
-
|
26
|
+
private
|
34
27
|
|
35
|
-
|
36
|
-
|
37
|
-
end
|
28
|
+
def http
|
29
|
+
HTTP::Factory.build timeout: timeout
|
38
30
|
end
|
39
31
|
end
|
40
32
|
end
|