mihari 5.0.1 → 5.1.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.
- checksums.yaml +4 -4
- data/.rspec +1 -1
- data/docker/Dockerfile +1 -1
- data/lib/mihari/analyzers/binaryedge.rb +9 -7
- data/lib/mihari/analyzers/censys.rb +3 -5
- data/lib/mihari/analyzers/circl.rb +4 -6
- data/lib/mihari/analyzers/crtsh.rb +6 -7
- data/lib/mihari/analyzers/dnpedia.rb +3 -7
- data/lib/mihari/analyzers/dnstwister.rb +3 -5
- data/lib/mihari/analyzers/feed.rb +12 -10
- data/lib/mihari/analyzers/greynoise.rb +3 -5
- data/lib/mihari/analyzers/onyphe.rb +3 -4
- data/lib/mihari/analyzers/otx.rb +1 -3
- data/lib/mihari/analyzers/passivetotal.rb +5 -7
- data/lib/mihari/analyzers/pulsedive.rb +5 -7
- data/lib/mihari/analyzers/shodan.rb +3 -9
- data/lib/mihari/analyzers/urlscan.rb +7 -6
- data/lib/mihari/analyzers/virustotal.rb +4 -6
- data/lib/mihari/analyzers/virustotal_intelligence.rb +4 -5
- data/lib/mihari/analyzers/zoomeye.rb +4 -10
- data/lib/mihari/cli/database.rb +11 -0
- data/lib/mihari/cli/main.rb +10 -4
- data/lib/mihari/cli/rule.rb +11 -0
- data/lib/mihari/clients/base.rb +53 -0
- data/lib/mihari/clients/binaryedge.rb +33 -0
- data/lib/mihari/clients/censys.rb +42 -0
- data/lib/mihari/clients/circl.rb +59 -0
- data/lib/mihari/clients/crtsh.rb +31 -0
- data/lib/mihari/clients/dnpedia.rb +64 -0
- data/lib/mihari/clients/dnstwister.rb +40 -0
- data/lib/mihari/clients/greynoise.rb +29 -0
- data/lib/mihari/clients/misp.rb +24 -0
- data/lib/mihari/clients/onyphe.rb +23 -0
- data/lib/mihari/clients/otx.rb +29 -0
- data/lib/mihari/clients/passivetotal.rb +65 -0
- data/lib/mihari/clients/publsedive.rb +39 -0
- data/lib/mihari/clients/shodan.rb +30 -0
- data/lib/mihari/clients/the_hive.rb +28 -0
- data/lib/mihari/clients/urlscan.rb +31 -0
- data/lib/mihari/clients/virustotal.rb +56 -0
- data/lib/mihari/clients/zoomeye.rb +68 -0
- data/lib/mihari/commands/database.rb +28 -0
- data/lib/mihari/commands/{initializer.rb → rule.rb} +27 -6
- data/lib/mihari/commands/searcher.rb +5 -0
- data/lib/mihari/database.rb +8 -22
- data/lib/mihari/emitters/misp.rb +13 -20
- data/lib/mihari/emitters/the_hive.rb +3 -5
- data/lib/mihari/emitters/webhook.rb +2 -2
- data/lib/mihari/feed/reader.rb +14 -11
- data/lib/mihari/http.rb +29 -21
- data/lib/mihari/mixins/database.rb +2 -0
- data/lib/mihari/mixins/retriable.rb +3 -1
- data/lib/mihari/schemas/analyzer.rb +5 -4
- data/lib/mihari/version.rb +1 -1
- data/lib/mihari.rb +21 -0
- data/mihari.gemspec +14 -20
- metadata +61 -238
- data/lib/mihari/analyzers/clients/otx.rb +0 -36
- data/lib/mihari/commands/validator.rb +0 -31
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mihari
|
4
|
+
module Clients
|
5
|
+
class Base
|
6
|
+
# @return [String]
|
7
|
+
attr_reader :base_url
|
8
|
+
|
9
|
+
# @return [Hash]
|
10
|
+
attr_reader :headers
|
11
|
+
|
12
|
+
#
|
13
|
+
# @param [String] base_url
|
14
|
+
# @param [Hash] headers
|
15
|
+
#
|
16
|
+
def initialize(base_url, headers: {})
|
17
|
+
@base_url = base_url
|
18
|
+
@headers = headers || {}
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
#
|
24
|
+
# @param [String] path
|
25
|
+
#
|
26
|
+
# @return [String]
|
27
|
+
#
|
28
|
+
def url_for(path)
|
29
|
+
base_url + path
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# @param [String] path
|
34
|
+
# @param [Hashk, nil] params
|
35
|
+
#
|
36
|
+
# @return [String] <description>
|
37
|
+
#
|
38
|
+
def get(path, params: nil)
|
39
|
+
HTTP.get(url_for(path), headers: headers, params: params)
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# @param [String] path
|
44
|
+
# @param [Hash, nil] json
|
45
|
+
#
|
46
|
+
# @return [String] <description>
|
47
|
+
#
|
48
|
+
def post(path, json: {})
|
49
|
+
HTTP.post(url_for(path), headers: headers, json: json)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mihari
|
4
|
+
module Clients
|
5
|
+
class BinaryEdge < Base
|
6
|
+
def initialize(base_url = "https://api.binaryedge.io/v2", api_key:, headers: {})
|
7
|
+
raise(ArgumentError, "'api_key' argument is required") unless api_key
|
8
|
+
|
9
|
+
headers["x-key"] = api_key
|
10
|
+
|
11
|
+
super(base_url, headers: headers)
|
12
|
+
end
|
13
|
+
|
14
|
+
#
|
15
|
+
# @param [String] query String used to query our data
|
16
|
+
# @param [Integer] page Default 1, Maximum: 500
|
17
|
+
# @param [Integer, nil] only_ips If selected, only output IP addresses, ports and protocols.
|
18
|
+
#
|
19
|
+
# @return [Hash]
|
20
|
+
#
|
21
|
+
def search(query, page: 1, only_ips: nil)
|
22
|
+
params = {
|
23
|
+
query: query,
|
24
|
+
page: page,
|
25
|
+
only_ips: only_ips
|
26
|
+
}.compact
|
27
|
+
|
28
|
+
res = get("/query/search", params: params)
|
29
|
+
JSON.parse(res.body.to_s)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "base64"
|
4
|
+
|
5
|
+
module Mihari
|
6
|
+
module Clients
|
7
|
+
class Censys < Base
|
8
|
+
#
|
9
|
+
# @param [String] base_url
|
10
|
+
# @param [String] id
|
11
|
+
# @param [String] secret
|
12
|
+
# @param [Hash] headers
|
13
|
+
#
|
14
|
+
def initialize(base_url = "https://search.censys.io", id:, secret:, headers: {})
|
15
|
+
raise(ArgumentError, "'id' argument is required") if id.nil?
|
16
|
+
raise(ArgumentError, "'secret' argument is required") if secret.nil?
|
17
|
+
|
18
|
+
headers["authorization"] = "Basic #{Base64.strict_encode64("#{id}:#{secret}")}"
|
19
|
+
|
20
|
+
super(base_url, headers: headers)
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# Search current index.
|
25
|
+
#
|
26
|
+
# Searches the given index for all records that match the given query.
|
27
|
+
# For more details, see our documentation: https://search.censys.io/api/v2/docs
|
28
|
+
#
|
29
|
+
# @param [String] query the query to be executed.
|
30
|
+
# @params [Integer, nil] per_page the number of results to be returned for each page.
|
31
|
+
# @params [Integer, nil] cursor the cursor of the desired result set.
|
32
|
+
#
|
33
|
+
# @return [Hash]
|
34
|
+
#
|
35
|
+
def search(query, per_page: nil, cursor: nil)
|
36
|
+
params = { q: query, per_page: per_page, cursor: cursor }.compact
|
37
|
+
res = get("/api/v2/hosts/search", params: params)
|
38
|
+
JSON.parse(res.body.to_s)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "base64"
|
4
|
+
|
5
|
+
module Mihari
|
6
|
+
module Clients
|
7
|
+
class CIRCL < Base
|
8
|
+
#
|
9
|
+
# @param [String] base_url
|
10
|
+
# @param [String] username
|
11
|
+
# @param [String] password
|
12
|
+
# @param [Hash] headers
|
13
|
+
#
|
14
|
+
def initialize(base_url = "https://www.circl.lu", username:, password:, headers: {})
|
15
|
+
raise(ArgumentError, "'username' argument is required") if username.nil?
|
16
|
+
raise(ArgumentError, "'password' argument is required") if password.nil?
|
17
|
+
|
18
|
+
headers["authorization"] = "Basic #{Base64.strict_encode64("#{username}:#{password}")}"
|
19
|
+
|
20
|
+
super(base_url, headers: headers)
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# @param [String] query
|
25
|
+
#
|
26
|
+
# @return [Hash]
|
27
|
+
#
|
28
|
+
def dns_query(query)
|
29
|
+
_get("/pdns/query/#{query}")
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# @param [String] query
|
34
|
+
#
|
35
|
+
# @return [Hash]
|
36
|
+
#
|
37
|
+
def ssl_cquery(query)
|
38
|
+
_get("/v2pssl/cquery/#{query}")
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
#
|
44
|
+
#
|
45
|
+
# @param [String] path
|
46
|
+
# @param [Array<Hash>] params
|
47
|
+
#
|
48
|
+
def _get(path, params: {})
|
49
|
+
res = get(path, params: params)
|
50
|
+
body = res.body.to_s
|
51
|
+
content_type = res["Content-Type"].to_s
|
52
|
+
|
53
|
+
return JSON.parse(body) if content_type.include?("application/json")
|
54
|
+
|
55
|
+
body.lines.map { |line| JSON.parse line }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mihari
|
4
|
+
module Clients
|
5
|
+
class Crtsh < Base
|
6
|
+
#
|
7
|
+
# @param [String] base_url
|
8
|
+
# @param [Hash] headers
|
9
|
+
#
|
10
|
+
def initialize(base_url = "https://crt.sh", headers: {})
|
11
|
+
super(base_url, headers: headers)
|
12
|
+
end
|
13
|
+
|
14
|
+
#
|
15
|
+
# Search crt.sh by a given identity
|
16
|
+
#
|
17
|
+
# @param [String] identity
|
18
|
+
# @param [String, nil] match "=", "ILIKE", "LIKE", "single", "any" or nil
|
19
|
+
# @param [String, nil] exclude "expired" or nil
|
20
|
+
#
|
21
|
+
# @return [Array<Hash>]
|
22
|
+
#
|
23
|
+
def search(identity, match: nil, exclude: nil)
|
24
|
+
params = { identity: identity, match: match, exclude: exclude, output: "json" }.compact
|
25
|
+
|
26
|
+
res = get("/", params: params)
|
27
|
+
JSON.parse(res.body.to_s)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "zlib"
|
5
|
+
|
6
|
+
module Mihari
|
7
|
+
module Clients
|
8
|
+
class DNPedia < Base
|
9
|
+
DEFAULT_HEADERS = {
|
10
|
+
"Accept-Encoding" => "gzip",
|
11
|
+
"Referer" => "https://dnpedia.com/tlds/search.php",
|
12
|
+
"X-Requested-With" => "XMLHttpRequest"
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
DEFAULT_PARAMS = {
|
16
|
+
cmd: "search",
|
17
|
+
columns: "id,name,zoneid,length,idn,thedate,",
|
18
|
+
ecf: "name",
|
19
|
+
ecv: "",
|
20
|
+
days: 2,
|
21
|
+
mode: "added",
|
22
|
+
_search: false,
|
23
|
+
nd: 1_569_842_920_216,
|
24
|
+
rows: 500,
|
25
|
+
page: 1,
|
26
|
+
sidx: "length",
|
27
|
+
sord: "asc"
|
28
|
+
}.freeze
|
29
|
+
|
30
|
+
#
|
31
|
+
# @param [String] base_url
|
32
|
+
# @param [Hash] headers
|
33
|
+
#
|
34
|
+
def initialize(base_url = "https://dnpedia.com", headers: {})
|
35
|
+
headers = headers.merge(DEFAULT_HEADERS)
|
36
|
+
|
37
|
+
super(base_url, headers: headers)
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# @param [String] keyword
|
42
|
+
#
|
43
|
+
def search(keyword)
|
44
|
+
params = DEFAULT_PARAMS.merge({ ecv: normalize(keyword) })
|
45
|
+
res = get("/tlds/ajax.php", params: params)
|
46
|
+
|
47
|
+
sio = StringIO.new(res.body.to_s)
|
48
|
+
gz = Zlib::GzipReader.new(sio)
|
49
|
+
page = gz.read
|
50
|
+
|
51
|
+
JSON.parse page
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def normalize(word)
|
57
|
+
return word if word.start_with?("~")
|
58
|
+
return word unless word.include?("%")
|
59
|
+
|
60
|
+
"~#{word}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mihari
|
4
|
+
module Clients
|
5
|
+
class DNSTwister < Base
|
6
|
+
#
|
7
|
+
# @param [String] base_url
|
8
|
+
# @param [Hash] headers
|
9
|
+
#
|
10
|
+
def initialize(base_url = "https://dnstwister.report", headers: {})
|
11
|
+
super(base_url, headers: headers)
|
12
|
+
end
|
13
|
+
|
14
|
+
#
|
15
|
+
# Get fuzzy domains
|
16
|
+
#
|
17
|
+
# @param [String] domain
|
18
|
+
#
|
19
|
+
# @return [Hash]
|
20
|
+
#
|
21
|
+
def fuzz(domain)
|
22
|
+
res = get("/api/fuzz/#{to_hex(domain)}")
|
23
|
+
JSON.parse(res.body.to_s)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
#
|
29
|
+
# Converts string to hex
|
30
|
+
#
|
31
|
+
# @param [String] str String
|
32
|
+
#
|
33
|
+
# @return [String] Hex
|
34
|
+
#
|
35
|
+
def to_hex(str)
|
36
|
+
str.each_byte.map { |b| b.to_s(16) }.join
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mihari
|
4
|
+
module Clients
|
5
|
+
class GreyNoise < Base
|
6
|
+
def initialize(base_url = "https://api.greynoise.io", api_key:, headers: {})
|
7
|
+
raise(ArgumentError, "'api_key' argument is required") unless api_key
|
8
|
+
|
9
|
+
headers["key"] = api_key
|
10
|
+
super(base_url, headers: headers)
|
11
|
+
end
|
12
|
+
|
13
|
+
#
|
14
|
+
# GNQL (GreyNoise Query Language) is a domain-specific query language that uses Lucene deep under the hood
|
15
|
+
#
|
16
|
+
# @param [String] query GNQL query string
|
17
|
+
# @param [Integer, nil] size Maximum amount of results to grab
|
18
|
+
# @param [Integer, nil] scroll Scroll token to paginate through results
|
19
|
+
#
|
20
|
+
# @return [Hash]
|
21
|
+
#
|
22
|
+
def gnql_search(query, size: nil, scroll: nil)
|
23
|
+
params = { query: query, size: size, scroll: scroll }.compact
|
24
|
+
res = get("/v2/experimental/gnql", params: params)
|
25
|
+
JSON.parse res.body.to_s
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mihari
|
4
|
+
module Clients
|
5
|
+
class MISP < Base
|
6
|
+
#
|
7
|
+
# @param [String] base_url
|
8
|
+
# @param [String] api_key
|
9
|
+
# @param [Hash] headers
|
10
|
+
#
|
11
|
+
def initialize(base_url, api_key:, headers: {})
|
12
|
+
raise(ArgumentError, "'api_key' argument is required") unless api_key
|
13
|
+
|
14
|
+
headers["authorization"] = api_key
|
15
|
+
super(base_url, headers: headers)
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_event(payload)
|
19
|
+
res = post("/events/add", json: payload)
|
20
|
+
JSON.parse(res.body.to_s)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mihari
|
4
|
+
module Clients
|
5
|
+
class Onyphe < Base
|
6
|
+
attr_reader :api_key
|
7
|
+
|
8
|
+
def initialize(base_url = "https://www.onyphe.io", api_key:, headers: {})
|
9
|
+
raise(ArgumentError, "'api_key' argument is required") if api_key.nil?
|
10
|
+
|
11
|
+
super(base_url, headers: headers)
|
12
|
+
|
13
|
+
@api_key = api_key
|
14
|
+
end
|
15
|
+
|
16
|
+
def datascan(query, page: 1)
|
17
|
+
params = { page: page, apikey: api_key }
|
18
|
+
res = get("/api/v2/simple/datascan/#{query}", params: params)
|
19
|
+
JSON.parse(res.body.to_s)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mihari
|
4
|
+
module Clients
|
5
|
+
class OTX < Base
|
6
|
+
def initialize(base_url = "https://otx.alienvault.com", api_key:, headers: {})
|
7
|
+
raise(ArgumentError, "'api_key' argument is required") unless api_key
|
8
|
+
|
9
|
+
headers["x-otx-api-key"] = api_key
|
10
|
+
super(base_url, headers: headers)
|
11
|
+
end
|
12
|
+
|
13
|
+
def query_by_ip(ip)
|
14
|
+
_get "/api/v1/indicators/IPv4/#{ip}/passive_dns"
|
15
|
+
end
|
16
|
+
|
17
|
+
def query_by_domain(domain)
|
18
|
+
_get "/api/v1/indicators/domain/#{domain}/passive_dns"
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def _get(path)
|
24
|
+
res = get(path)
|
25
|
+
JSON.parse(res.body.to_s)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "base64"
|
4
|
+
|
5
|
+
module Mihari
|
6
|
+
module Clients
|
7
|
+
class PassiveTotal < Base
|
8
|
+
#
|
9
|
+
# @param [String] base_url
|
10
|
+
# @param [String] username
|
11
|
+
# @param [String] api_key
|
12
|
+
# @param [Hash] headers
|
13
|
+
#
|
14
|
+
def initialize(base_url = "https://api.passivetotal.org", username:, api_key:, headers: {})
|
15
|
+
raise(ArgumentError, "'username' argument is required") if username.nil?
|
16
|
+
raise(ArgumentError, "'api_key' argument is required") if api_key.nil?
|
17
|
+
|
18
|
+
headers["authorization"] = "Basic #{Base64.strict_encode64("#{username}:#{api_key}")}"
|
19
|
+
|
20
|
+
super(base_url, headers: headers)
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# @param [String] query
|
25
|
+
#
|
26
|
+
def ssl_search(query)
|
27
|
+
params = { query: query }
|
28
|
+
_get("/v2/ssl-certificate/history", params: params)
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# @param [String] query
|
33
|
+
#
|
34
|
+
def passive_dns_search(query)
|
35
|
+
params = { query: query }
|
36
|
+
_get("/v2/dns/passive/unique", params: params)
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# @param [String] query the domain being queried
|
41
|
+
# @param [String] field whether to return historical results
|
42
|
+
#
|
43
|
+
# @return [Hash]
|
44
|
+
#
|
45
|
+
def reverse_whois_search(query:, field:)
|
46
|
+
params = {
|
47
|
+
query: query,
|
48
|
+
field: field
|
49
|
+
}.compact
|
50
|
+
_get("/v2/whois/search", params: params)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
#
|
56
|
+
# @param [String] path
|
57
|
+
# @param [Hash] params
|
58
|
+
#
|
59
|
+
def _get(path, params: {})
|
60
|
+
res = get(path, params: params)
|
61
|
+
JSON.parse(res.body.to_s)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mihari
|
4
|
+
module Clients
|
5
|
+
class PulseDive < Base
|
6
|
+
attr_reader :api_key
|
7
|
+
|
8
|
+
def initialize(base_url = "https://pulsedive.com", api_key:, headers: {})
|
9
|
+
super(base_url, headers: headers)
|
10
|
+
|
11
|
+
@api_key = api_key
|
12
|
+
|
13
|
+
raise(ArgumentError, "'api_key' argument is required") unless api_key
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_indicator(ip_or_domain)
|
17
|
+
_get "/api/info.php", params: { indicator: ip_or_domain }
|
18
|
+
end
|
19
|
+
|
20
|
+
def get_properties(indicator_id)
|
21
|
+
_get "/api/info.php", params: { iid: indicator_id, get: "properties" }
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
#
|
27
|
+
#
|
28
|
+
# @param [String] path
|
29
|
+
# @param [Hash] params
|
30
|
+
#
|
31
|
+
def _get(path, params: {})
|
32
|
+
params["key"] = api_key
|
33
|
+
|
34
|
+
res = get(path, params: params)
|
35
|
+
JSON.parse(res.body.to_s)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mihari
|
4
|
+
module Clients
|
5
|
+
class Shodan < Base
|
6
|
+
attr_reader :api_key
|
7
|
+
|
8
|
+
def initialize(base_url = "https://api.shodan.io", api_key:, headers: {})
|
9
|
+
raise(ArgumentError, "'api_key' argument is required") unless api_key
|
10
|
+
|
11
|
+
super(base_url, headers: headers)
|
12
|
+
|
13
|
+
@api_key = api_key
|
14
|
+
end
|
15
|
+
|
16
|
+
# Search Shodan using the same query syntax as the website and use facets
|
17
|
+
# to get summary information for different properties.
|
18
|
+
def search(query, page: 1, minify: true)
|
19
|
+
params = {
|
20
|
+
query: query,
|
21
|
+
page: page,
|
22
|
+
minify: minify,
|
23
|
+
key: api_key
|
24
|
+
}
|
25
|
+
res = get("/shodan/host/search", params: params)
|
26
|
+
JSON.parse(res.body.to_s)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mihari
|
4
|
+
module Clients
|
5
|
+
class TheHive < Base
|
6
|
+
#
|
7
|
+
# @param [String] base_url
|
8
|
+
# @param [String] api_key
|
9
|
+
# @param [String, nil] api_version
|
10
|
+
# @param [Hash] headers
|
11
|
+
#
|
12
|
+
def initialize(base_url, api_key:, api_version:, headers: {})
|
13
|
+
raise(ArgumentError, "'api_key' argument is required") unless api_key
|
14
|
+
|
15
|
+
base_url += "/#{api_version}" unless api_version.nil?
|
16
|
+
headers["authorization"] = "Bearer #{api_key}"
|
17
|
+
|
18
|
+
super(base_url, headers: headers)
|
19
|
+
end
|
20
|
+
|
21
|
+
def alert(json)
|
22
|
+
json = json.to_camelback_keys.compact
|
23
|
+
res = post("/alert", json: json)
|
24
|
+
JSON.parse(res.body.to_s)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mihari
|
4
|
+
module Clients
|
5
|
+
class UrlScan < Base
|
6
|
+
#
|
7
|
+
# @param [String] base_url
|
8
|
+
# @param [String] api_key
|
9
|
+
# @param [Hash] headers
|
10
|
+
#
|
11
|
+
def initialize(base_url = "https://urlscan.io", api_key:, headers: {})
|
12
|
+
raise(ArgumentError, "'api_key' argument is required") if api_key.nil?
|
13
|
+
|
14
|
+
headers["api-key"] = api_key
|
15
|
+
|
16
|
+
super(base_url, headers: headers)
|
17
|
+
end
|
18
|
+
|
19
|
+
#
|
20
|
+
# @param [String] q
|
21
|
+
# @param [Integer] size
|
22
|
+
# @param [String, nil] search_after
|
23
|
+
#
|
24
|
+
def search(q, size: 100, search_after: nil)
|
25
|
+
params = { q: q, size: size, search_after: search_after }.compact
|
26
|
+
res = get("/api/v1/search/", params: params)
|
27
|
+
JSON.parse res.body.to_s
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mihari
|
4
|
+
module Clients
|
5
|
+
class VirusTotal < Base
|
6
|
+
#
|
7
|
+
# @param [String] base_url
|
8
|
+
# @param [String] id
|
9
|
+
# @param [String] secret
|
10
|
+
# @param [Hash] headers
|
11
|
+
#
|
12
|
+
def initialize(base_url = "https://www.virustotal.com", api_key:, headers: {})
|
13
|
+
raise(ArgumentError, "'api_key' argument is required") if api_key.nil?
|
14
|
+
|
15
|
+
headers["x-apikey"] = api_key
|
16
|
+
|
17
|
+
super(base_url, headers: headers)
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# @param [String] query
|
22
|
+
#
|
23
|
+
def domain_search(query)
|
24
|
+
_get("/api/v3/domains/#{query}/resolutions")
|
25
|
+
end
|
26
|
+
|
27
|
+
#
|
28
|
+
# @param [String] query
|
29
|
+
#
|
30
|
+
def ip_search(query)
|
31
|
+
_get("/api/v3/ip_addresses/#{query}/resolutions")
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
# @param [String] query
|
36
|
+
# @param [String, nil] cursor
|
37
|
+
#
|
38
|
+
def intel_search(query, cursor: nil)
|
39
|
+
params = { query: query, cursor: cursor }.compact
|
40
|
+
_get("/api/v3/intelligence/search", params: params)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
#
|
46
|
+
#
|
47
|
+
# @param [String] path
|
48
|
+
# @param [Hash] params
|
49
|
+
#
|
50
|
+
def _get(path, params: {})
|
51
|
+
res = get(path, params: params)
|
52
|
+
JSON.parse(res.body.to_s)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|