mihari 1.3.2 → 2.0.0
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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +68 -0
- data/README.md +20 -270
- data/Rakefile +1 -0
- data/build_frontend.sh +14 -0
- data/docker/Dockerfile +3 -2
- data/{screenshots → images}/alert.png +0 -0
- data/{screenshots → images}/eyecatch.png +0 -0
- data/images/logo.png +0 -0
- data/{screenshots → images}/misp.png +0 -0
- data/{screenshots → images}/slack.png +0 -0
- data/images/web_alerts.png +0 -0
- data/images/web_config.png +0 -0
- data/lib/mihari.rb +2 -2
- data/lib/mihari/analyzers/base.rb +1 -1
- data/lib/mihari/analyzers/basic.rb +3 -4
- data/lib/mihari/analyzers/binaryedge.rb +4 -7
- data/lib/mihari/analyzers/censys.rb +3 -7
- data/lib/mihari/analyzers/circl.rb +3 -5
- data/lib/mihari/analyzers/crtsh.rb +2 -6
- data/lib/mihari/analyzers/dnpedia.rb +3 -6
- data/lib/mihari/analyzers/dnstwister.rb +4 -9
- data/lib/mihari/analyzers/free_text.rb +2 -6
- data/lib/mihari/analyzers/http_hash.rb +3 -11
- data/lib/mihari/analyzers/onyphe.rb +3 -6
- data/lib/mihari/analyzers/otx.rb +4 -9
- data/lib/mihari/analyzers/passive_dns.rb +4 -9
- data/lib/mihari/analyzers/passive_ssl.rb +4 -9
- data/lib/mihari/analyzers/passivetotal.rb +9 -14
- data/lib/mihari/analyzers/pulsedive.rb +7 -12
- data/lib/mihari/analyzers/reverse_whois.rb +4 -9
- data/lib/mihari/analyzers/securitytrails.rb +12 -17
- data/lib/mihari/analyzers/securitytrails_domain_feed.rb +3 -7
- data/lib/mihari/analyzers/shodan.rb +9 -8
- data/lib/mihari/analyzers/spyse.rb +6 -11
- data/lib/mihari/analyzers/ssh_fingerprint.rb +2 -6
- data/lib/mihari/analyzers/urlscan.rb +21 -9
- data/lib/mihari/analyzers/virustotal.rb +6 -11
- data/lib/mihari/analyzers/zoomeye.rb +7 -11
- data/lib/mihari/cli.rb +20 -28
- data/lib/mihari/config.rb +1 -25
- data/lib/mihari/configurable.rb +4 -5
- data/lib/mihari/database.rb +7 -1
- data/lib/mihari/emitters/misp.rb +4 -2
- data/lib/mihari/emitters/slack.rb +18 -7
- data/lib/mihari/emitters/the_hive.rb +2 -2
- data/lib/mihari/errors.rb +2 -0
- data/lib/mihari/models/alert.rb +51 -0
- data/lib/mihari/models/artifact.rb +1 -1
- data/lib/mihari/notifiers/exception_notifier.rb +5 -5
- data/lib/mihari/serializers/alert.rb +1 -1
- data/lib/mihari/serializers/artifact.rb +1 -1
- data/lib/mihari/serializers/tag.rb +1 -1
- data/lib/mihari/status.rb +10 -10
- data/lib/mihari/type_checker.rb +4 -4
- data/lib/mihari/version.rb +1 -1
- data/lib/mihari/web/app.rb +126 -0
- data/lib/mihari/web/public/index.html +21 -0
- data/lib/mihari/web/public/static/favicon.ico +0 -0
- data/lib/mihari/web/public/static/fonts/fa-brands-400.099a9556.woff +0 -0
- data/lib/mihari/web/public/static/fonts/fa-brands-400.30cc681d.eot +0 -0
- data/lib/mihari/web/public/static/fonts/fa-brands-400.3b89dd10.ttf +0 -0
- data/lib/mihari/web/public/static/fonts/fa-brands-400.f7307680.woff2 +0 -0
- data/lib/mihari/web/public/static/fonts/fa-regular-400.1f77739c.ttf +0 -0
- data/lib/mihari/web/public/static/fonts/fa-regular-400.7124eb50.woff +0 -0
- data/lib/mihari/web/public/static/fonts/fa-regular-400.7630483d.eot +0 -0
- data/lib/mihari/web/public/static/fonts/fa-regular-400.f0f82301.woff2 +0 -0
- data/lib/mihari/web/public/static/fonts/fa-solid-900.1042e8ca.eot +0 -0
- data/lib/mihari/web/public/static/fonts/fa-solid-900.605ed792.ttf +0 -0
- data/lib/mihari/web/public/static/fonts/fa-solid-900.9fe5a17c.woff +0 -0
- data/lib/mihari/web/public/static/fonts/fa-solid-900.e8a427e1.woff2 +0 -0
- data/lib/mihari/web/public/static/img/fa-brands-400.ba7ed552.svg +3717 -0
- data/lib/mihari/web/public/static/img/fa-regular-400.0bb42845.svg +801 -0
- data/lib/mihari/web/public/static/img/fa-solid-900.376c1f97.svg +5034 -0
- data/lib/mihari/web/public/static/js/app.58b32d15.js +12 -0
- data/lib/mihari/web/public/static/js/app.58b32d15.js.map +1 -0
- data/mihari.gemspec +30 -25
- metadata +163 -56
- data/.travis.yml +0 -13
- data/lib/mihari/alert_viewer.rb +0 -23
@@ -5,12 +5,7 @@ require "pulsedive"
|
|
5
5
|
module Mihari
|
6
6
|
module Analyzers
|
7
7
|
class Pulsedive < Base
|
8
|
-
attr_reader :query
|
9
|
-
attr_reader :type
|
10
|
-
|
11
|
-
attr_reader :title
|
12
|
-
attr_reader :description
|
13
|
-
attr_reader :tags
|
8
|
+
attr_reader :query, :type, :title, :description, :tags
|
14
9
|
|
15
10
|
def initialize(query, title: nil, description: nil, tags: [])
|
16
11
|
super()
|
@@ -30,7 +25,7 @@ module Mihari
|
|
30
25
|
private
|
31
26
|
|
32
27
|
def config_keys
|
33
|
-
%w
|
28
|
+
%w[pulsedive_api_key]
|
34
29
|
end
|
35
30
|
|
36
31
|
def api
|
@@ -38,18 +33,18 @@ module Mihari
|
|
38
33
|
end
|
39
34
|
|
40
35
|
def valid_type?
|
41
|
-
%w
|
36
|
+
%w[ip domain].include? type
|
42
37
|
end
|
43
38
|
|
44
39
|
def lookup
|
45
|
-
raise InvalidInputError, "#{query}(type: #{type ||
|
40
|
+
raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
|
46
41
|
|
47
42
|
indicator = api.indicator.get_by_value(query)
|
48
|
-
iid = indicator
|
43
|
+
iid = indicator["iid"]
|
49
44
|
|
50
45
|
properties = api.indicator.get_properties_by_id(iid)
|
51
|
-
(properties
|
52
|
-
property
|
46
|
+
(properties["dns"] || []).map do |property|
|
47
|
+
property["value"] if ["A", "PTR"].include?(property["name"])
|
53
48
|
end.compact
|
54
49
|
end
|
55
50
|
end
|
@@ -5,16 +5,11 @@ require "parallel"
|
|
5
5
|
module Mihari
|
6
6
|
module Analyzers
|
7
7
|
class ReveseWhois < Base
|
8
|
-
attr_reader :query
|
9
|
-
attr_reader :type
|
10
|
-
|
11
|
-
attr_reader :title
|
12
|
-
attr_reader :description
|
13
|
-
attr_reader :tags
|
8
|
+
attr_reader :query, :type, :title, :description, :tags
|
14
9
|
|
15
10
|
ANALYZERS = [
|
16
11
|
Mihari::Analyzers::PassiveTotal,
|
17
|
-
Mihari::Analyzers::SecurityTrails
|
12
|
+
Mihari::Analyzers::SecurityTrails
|
18
13
|
].freeze
|
19
14
|
|
20
15
|
def initialize(query, title: nil, description: nil, tags: [])
|
@@ -37,11 +32,11 @@ module Mihari
|
|
37
32
|
private
|
38
33
|
|
39
34
|
def valid_type?
|
40
|
-
%w
|
35
|
+
%w[mail].include? type
|
41
36
|
end
|
42
37
|
|
43
38
|
def analyzers
|
44
|
-
raise InvalidInputError, "#{query}(type: #{type ||
|
39
|
+
raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
|
45
40
|
|
46
41
|
ANALYZERS.map do |klass|
|
47
42
|
klass.new(query)
|
@@ -5,12 +5,7 @@ require "securitytrails"
|
|
5
5
|
module Mihari
|
6
6
|
module Analyzers
|
7
7
|
class SecurityTrails < Base
|
8
|
-
attr_reader :query
|
9
|
-
attr_reader :type
|
10
|
-
|
11
|
-
attr_reader :title
|
12
|
-
attr_reader :description
|
13
|
-
attr_reader :tags
|
8
|
+
attr_reader :query, :type, :title, :description, :tags
|
14
9
|
|
15
10
|
def initialize(query, title: nil, description: nil, tags: [])
|
16
11
|
super()
|
@@ -30,7 +25,7 @@ module Mihari
|
|
30
25
|
private
|
31
26
|
|
32
27
|
def config_keys
|
33
|
-
%w
|
28
|
+
%w[securitytrails_api_key]
|
34
29
|
end
|
35
30
|
|
36
31
|
def api
|
@@ -38,7 +33,7 @@ module Mihari
|
|
38
33
|
end
|
39
34
|
|
40
35
|
def valid_type?
|
41
|
-
%w
|
36
|
+
%w[ip domain mail].include? type
|
42
37
|
end
|
43
38
|
|
44
39
|
def lookup
|
@@ -50,28 +45,28 @@ module Mihari
|
|
50
45
|
when "mail"
|
51
46
|
mail_lookup
|
52
47
|
else
|
53
|
-
raise InvalidInputError, "#{query}(type: #{type ||
|
48
|
+
raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
|
54
49
|
end
|
55
50
|
end
|
56
51
|
|
57
52
|
def domain_lookup
|
58
53
|
result = api.history.get_all_dns_history(query, type: "a")
|
59
|
-
records = result
|
54
|
+
records = result["records"] || []
|
60
55
|
records.map do |record|
|
61
|
-
(record
|
56
|
+
(record["values"] || []).map { |value| value["ip"] }
|
62
57
|
end.flatten.compact.uniq
|
63
58
|
end
|
64
59
|
|
65
60
|
def ip_lookup
|
66
|
-
result = api.domains.search(
|
67
|
-
records = result
|
68
|
-
records.map { |record| record
|
61
|
+
result = api.domains.search(filter: {ipv4: query})
|
62
|
+
records = result["records"] || []
|
63
|
+
records.map { |record| record["hostname"] }.compact.uniq
|
69
64
|
end
|
70
65
|
|
71
66
|
def mail_lookup
|
72
|
-
result = api.domains.search(
|
73
|
-
records = result
|
74
|
-
records.map { |record| record
|
67
|
+
result = api.domains.search(filter: {whois_email: query})
|
68
|
+
records = result["records"] || []
|
69
|
+
records.map { |record| record["hostname"] }.compact.uniq
|
75
70
|
end
|
76
71
|
end
|
77
72
|
end
|
@@ -5,11 +5,7 @@ require "securitytrails"
|
|
5
5
|
module Mihari
|
6
6
|
module Analyzers
|
7
7
|
class SecurityTrailsDomainFeed < Base
|
8
|
-
attr_reader :type
|
9
|
-
|
10
|
-
attr_reader :title
|
11
|
-
attr_reader :description
|
12
|
-
attr_reader :tags
|
8
|
+
attr_reader :type, :title, :description, :tags
|
13
9
|
|
14
10
|
def initialize(regexp, type: "registered", title: nil, description: nil, tags: [])
|
15
11
|
super()
|
@@ -32,7 +28,7 @@ module Mihari
|
|
32
28
|
private
|
33
29
|
|
34
30
|
def config_keys
|
35
|
-
%w
|
31
|
+
%w[securitytrails_api_key]
|
36
32
|
end
|
37
33
|
|
38
34
|
def api
|
@@ -40,7 +36,7 @@ module Mihari
|
|
40
36
|
end
|
41
37
|
|
42
38
|
def valid_type?
|
43
|
-
%w
|
39
|
+
%w[all new registered].include? type
|
44
40
|
end
|
45
41
|
|
46
42
|
def regexp
|
@@ -5,10 +5,7 @@ require "shodan"
|
|
5
5
|
module Mihari
|
6
6
|
module Analyzers
|
7
7
|
class Shodan < Base
|
8
|
-
attr_reader :title
|
9
|
-
attr_reader :description
|
10
|
-
attr_reader :query
|
11
|
-
attr_reader :tags
|
8
|
+
attr_reader :title, :description, :query, :tags
|
12
9
|
|
13
10
|
def initialize(query, title: nil, description: nil, tags: [])
|
14
11
|
super()
|
@@ -24,9 +21,9 @@ module Mihari
|
|
24
21
|
return [] unless results || results.empty?
|
25
22
|
|
26
23
|
results.map do |result|
|
27
|
-
matches = result
|
24
|
+
matches = result["matches"] || []
|
28
25
|
matches.map do |match|
|
29
|
-
match
|
26
|
+
match["ip_str"]
|
30
27
|
end.compact
|
31
28
|
end.flatten.compact.uniq
|
32
29
|
end
|
@@ -36,7 +33,7 @@ module Mihari
|
|
36
33
|
PAGE_SIZE = 100
|
37
34
|
|
38
35
|
def config_keys
|
39
|
-
%w
|
36
|
+
%w[shodan_api_key]
|
40
37
|
end
|
41
38
|
|
42
39
|
def api
|
@@ -58,7 +55,11 @@ module Mihari
|
|
58
55
|
break unless res
|
59
56
|
|
60
57
|
responses << res
|
61
|
-
break if res
|
58
|
+
break if res["total"].to_i <= page * PAGE_SIZE
|
59
|
+
rescue JSON::ParserError
|
60
|
+
# ignore JSON::ParserError
|
61
|
+
# ref. https://github.com/ninoseki/mihari/issues/197
|
62
|
+
next
|
62
63
|
end
|
63
64
|
responses
|
64
65
|
end
|
@@ -6,12 +6,7 @@ require "json"
|
|
6
6
|
module Mihari
|
7
7
|
module Analyzers
|
8
8
|
class Spyse < Base
|
9
|
-
attr_reader :query
|
10
|
-
attr_reader :type
|
11
|
-
|
12
|
-
attr_reader :title
|
13
|
-
attr_reader :description
|
14
|
-
attr_reader :tags
|
9
|
+
attr_reader :query, :type, :title, :description, :tags
|
15
10
|
|
16
11
|
def initialize(query, title: nil, description: nil, tags: [], type: "domain")
|
17
12
|
super()
|
@@ -35,7 +30,7 @@ module Mihari
|
|
35
30
|
end
|
36
31
|
|
37
32
|
def config_keys
|
38
|
-
%w
|
33
|
+
%w[spyse_api_key]
|
39
34
|
end
|
40
35
|
|
41
36
|
def api
|
@@ -43,14 +38,14 @@ module Mihari
|
|
43
38
|
end
|
44
39
|
|
45
40
|
def valid_type?
|
46
|
-
%w
|
41
|
+
%w[ip domain cert].include? type
|
47
42
|
end
|
48
43
|
|
49
44
|
def domain_lookup
|
50
45
|
res = api.domain.search(search_params, limit: 100)
|
51
46
|
items = res.dig("data", "items") || []
|
52
47
|
items.map do |item|
|
53
|
-
item
|
48
|
+
item["name"]
|
54
49
|
end.uniq.compact
|
55
50
|
end
|
56
51
|
|
@@ -58,7 +53,7 @@ module Mihari
|
|
58
53
|
res = api.ip.search(search_params, limit: 100)
|
59
54
|
items = res.dig("data", "items") || []
|
60
55
|
items.map do |item|
|
61
|
-
item
|
56
|
+
item["ip"]
|
62
57
|
end.uniq.compact
|
63
58
|
end
|
64
59
|
|
@@ -69,7 +64,7 @@ module Mihari
|
|
69
64
|
when "ip"
|
70
65
|
ip_lookup
|
71
66
|
else
|
72
|
-
raise InvalidInputError, "#{query}(type: #{type ||
|
67
|
+
raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
|
73
68
|
end
|
74
69
|
end
|
75
70
|
end
|
@@ -5,11 +5,7 @@ require "parallel"
|
|
5
5
|
module Mihari
|
6
6
|
module Analyzers
|
7
7
|
class SSHFingerprint < Base
|
8
|
-
attr_reader :fingerprint
|
9
|
-
|
10
|
-
attr_reader :title
|
11
|
-
attr_reader :description
|
12
|
-
attr_reader :tags
|
8
|
+
attr_reader :fingerprint, :title, :description, :tags
|
13
9
|
|
14
10
|
def initialize(fingerprint, title: nil, description: nil, tags: [])
|
15
11
|
super()
|
@@ -46,7 +42,7 @@ module Mihari
|
|
46
42
|
|
47
43
|
[
|
48
44
|
binary_edge,
|
49
|
-
shodan
|
45
|
+
shodan
|
50
46
|
].compact
|
51
47
|
end
|
52
48
|
|
@@ -5,20 +5,29 @@ require "urlscan"
|
|
5
5
|
module Mihari
|
6
6
|
module Analyzers
|
7
7
|
class Urlscan < Base
|
8
|
-
attr_reader :title
|
9
|
-
attr_reader :description
|
10
|
-
attr_reader :query
|
11
|
-
attr_reader :tags
|
12
|
-
attr_reader :target_type
|
8
|
+
attr_reader :title, :description, :query, :tags, :filter, :target_type, :use_pro, :use_similarity
|
13
9
|
|
14
|
-
def initialize(
|
10
|
+
def initialize(
|
11
|
+
query,
|
12
|
+
description: nil,
|
13
|
+
filter: nil,
|
14
|
+
tags: [],
|
15
|
+
target_type: "url",
|
16
|
+
title: nil,
|
17
|
+
use_pro: false,
|
18
|
+
use_similarity: false
|
19
|
+
)
|
15
20
|
super()
|
16
21
|
|
17
22
|
@query = query
|
18
23
|
@title = title || "urlscan lookup"
|
19
24
|
@description = description || "query = #{query}"
|
20
25
|
@tags = tags
|
26
|
+
|
27
|
+
@filter = filter
|
21
28
|
@target_type = target_type
|
29
|
+
@use_pro = use_pro
|
30
|
+
@use_similarity = use_similarity
|
22
31
|
|
23
32
|
raise InvalidInputError, "type should be url, domain or ip." unless valid_target_type?
|
24
33
|
end
|
@@ -27,7 +36,7 @@ module Mihari
|
|
27
36
|
result = search
|
28
37
|
return [] unless result
|
29
38
|
|
30
|
-
results = result
|
39
|
+
results = result["results"] || []
|
31
40
|
results.map do |match|
|
32
41
|
match.dig "page", target_type
|
33
42
|
end.compact.uniq
|
@@ -36,7 +45,7 @@ module Mihari
|
|
36
45
|
private
|
37
46
|
|
38
47
|
def config_keys
|
39
|
-
%w
|
48
|
+
%w[urlscan_api_key]
|
40
49
|
end
|
41
50
|
|
42
51
|
def api
|
@@ -44,11 +53,14 @@ module Mihari
|
|
44
53
|
end
|
45
54
|
|
46
55
|
def search
|
56
|
+
return api.pro.similar(query) if use_similarity
|
57
|
+
return api.pro.search(query: query, filter: filter, size: 10_000) if use_pro
|
58
|
+
|
47
59
|
api.search(query, size: 10_000)
|
48
60
|
end
|
49
61
|
|
50
62
|
def valid_target_type?
|
51
|
-
%w
|
63
|
+
%w[url domain ip].include? target_type
|
52
64
|
end
|
53
65
|
end
|
54
66
|
end
|
@@ -5,12 +5,7 @@ require "virustotal"
|
|
5
5
|
module Mihari
|
6
6
|
module Analyzers
|
7
7
|
class VirusTotal < Base
|
8
|
-
attr_reader :indicator
|
9
|
-
attr_reader :type
|
10
|
-
|
11
|
-
attr_reader :title
|
12
|
-
attr_reader :description
|
13
|
-
attr_reader :tags
|
8
|
+
attr_reader :indicator, :type, :title, :description, :tags
|
14
9
|
|
15
10
|
def initialize(indicator, title: nil, description: nil, tags: [])
|
16
11
|
super()
|
@@ -30,7 +25,7 @@ module Mihari
|
|
30
25
|
private
|
31
26
|
|
32
27
|
def config_keys
|
33
|
-
%w
|
28
|
+
%w[virustotal_api_key]
|
34
29
|
end
|
35
30
|
|
36
31
|
def api
|
@@ -38,7 +33,7 @@ module Mihari
|
|
38
33
|
end
|
39
34
|
|
40
35
|
def valid_type?
|
41
|
-
%w
|
36
|
+
%w[ip domain].include? type
|
42
37
|
end
|
43
38
|
|
44
39
|
def lookup
|
@@ -48,14 +43,14 @@ module Mihari
|
|
48
43
|
when "ip"
|
49
44
|
ip_lookup
|
50
45
|
else
|
51
|
-
raise InvalidInputError, "#{indicator}(type: #{type ||
|
46
|
+
raise InvalidInputError, "#{indicator}(type: #{type || "unknown"}) is not supported." unless valid_type?
|
52
47
|
end
|
53
48
|
end
|
54
49
|
|
55
50
|
def domain_lookup
|
56
51
|
res = api.domain.resolutions(indicator)
|
57
52
|
|
58
|
-
data = res
|
53
|
+
data = res["data"] || []
|
59
54
|
data.map do |item|
|
60
55
|
item.dig("attributes", "ip_address")
|
61
56
|
end.compact.uniq
|
@@ -64,7 +59,7 @@ module Mihari
|
|
64
59
|
def ip_lookup
|
65
60
|
res = api.ip_address.resolutions(indicator)
|
66
61
|
|
67
|
-
data = res
|
62
|
+
data = res["data"] || []
|
68
63
|
data.map do |item|
|
69
64
|
item.dig("attributes", "host_name")
|
70
65
|
end.compact.uniq
|
@@ -5,11 +5,7 @@ require "zoomeye"
|
|
5
5
|
module Mihari
|
6
6
|
module Analyzers
|
7
7
|
class ZoomEye < Base
|
8
|
-
attr_reader :title
|
9
|
-
attr_reader :description
|
10
|
-
attr_reader :query
|
11
|
-
attr_reader :tags
|
12
|
-
attr_reader :type
|
8
|
+
attr_reader :title, :description, :query, :tags, :type
|
13
9
|
|
14
10
|
def initialize(query, title: nil, description: nil, tags: [], type: "host")
|
15
11
|
super()
|
@@ -37,11 +33,11 @@ module Mihari
|
|
37
33
|
PAGE_SIZE = 10
|
38
34
|
|
39
35
|
def valid_type?
|
40
|
-
%w
|
36
|
+
%w[host web].include? type
|
41
37
|
end
|
42
38
|
|
43
39
|
def config_keys
|
44
|
-
%w
|
40
|
+
%w[zoomeye_password zoomeye_username]
|
45
41
|
end
|
46
42
|
|
47
43
|
def api
|
@@ -50,9 +46,9 @@ module Mihari
|
|
50
46
|
|
51
47
|
def convert_responses(responses)
|
52
48
|
responses.map do |res|
|
53
|
-
matches = res
|
49
|
+
matches = res["matches"] || []
|
54
50
|
matches.map do |match|
|
55
|
-
match
|
51
|
+
match["ip"]
|
56
52
|
end
|
57
53
|
end.flatten.compact.uniq
|
58
54
|
end
|
@@ -69,7 +65,7 @@ module Mihari
|
|
69
65
|
res = _host_lookup(query, page: page)
|
70
66
|
break unless res
|
71
67
|
|
72
|
-
total = res
|
68
|
+
total = res["total"].to_i
|
73
69
|
responses << res
|
74
70
|
break if total <= page * PAGE_SIZE
|
75
71
|
end
|
@@ -88,7 +84,7 @@ module Mihari
|
|
88
84
|
res = _web_lookup(query, page: page)
|
89
85
|
break unless res
|
90
86
|
|
91
|
-
total = res
|
87
|
+
total = res["total"].to_i
|
92
88
|
responses << res
|
93
89
|
break if total <= page * PAGE_SIZE
|
94
90
|
end
|