mihari 5.1.0 → 5.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mihari/analyzers/binaryedge.rb +9 -7
  3. data/lib/mihari/analyzers/censys.rb +3 -5
  4. data/lib/mihari/analyzers/circl.rb +4 -6
  5. data/lib/mihari/analyzers/crtsh.rb +6 -7
  6. data/lib/mihari/analyzers/dnpedia.rb +3 -7
  7. data/lib/mihari/analyzers/dnstwister.rb +3 -5
  8. data/lib/mihari/analyzers/feed.rb +12 -10
  9. data/lib/mihari/analyzers/greynoise.rb +3 -5
  10. data/lib/mihari/analyzers/onyphe.rb +3 -4
  11. data/lib/mihari/analyzers/otx.rb +1 -3
  12. data/lib/mihari/analyzers/passivetotal.rb +5 -7
  13. data/lib/mihari/analyzers/pulsedive.rb +5 -7
  14. data/lib/mihari/analyzers/shodan.rb +3 -9
  15. data/lib/mihari/analyzers/urlscan.rb +7 -6
  16. data/lib/mihari/analyzers/virustotal.rb +4 -6
  17. data/lib/mihari/analyzers/virustotal_intelligence.rb +4 -5
  18. data/lib/mihari/analyzers/zoomeye.rb +4 -10
  19. data/lib/mihari/clients/base.rb +53 -0
  20. data/lib/mihari/clients/binaryedge.rb +33 -0
  21. data/lib/mihari/clients/censys.rb +42 -0
  22. data/lib/mihari/clients/circl.rb +59 -0
  23. data/lib/mihari/clients/crtsh.rb +31 -0
  24. data/lib/mihari/clients/dnpedia.rb +64 -0
  25. data/lib/mihari/clients/dnstwister.rb +40 -0
  26. data/lib/mihari/clients/greynoise.rb +29 -0
  27. data/lib/mihari/clients/misp.rb +24 -0
  28. data/lib/mihari/clients/onyphe.rb +23 -0
  29. data/lib/mihari/clients/otx.rb +29 -0
  30. data/lib/mihari/clients/passivetotal.rb +65 -0
  31. data/lib/mihari/clients/publsedive.rb +39 -0
  32. data/lib/mihari/clients/shodan.rb +30 -0
  33. data/lib/mihari/clients/the_hive.rb +28 -0
  34. data/lib/mihari/clients/urlscan.rb +31 -0
  35. data/lib/mihari/clients/virustotal.rb +56 -0
  36. data/lib/mihari/clients/zoomeye.rb +68 -0
  37. data/lib/mihari/emitters/misp.rb +13 -20
  38. data/lib/mihari/emitters/the_hive.rb +3 -5
  39. data/lib/mihari/emitters/webhook.rb +2 -2
  40. data/lib/mihari/feed/reader.rb +14 -11
  41. data/lib/mihari/http.rb +29 -21
  42. data/lib/mihari/mixins/retriable.rb +3 -1
  43. data/lib/mihari/schemas/analyzer.rb +5 -4
  44. data/lib/mihari/version.rb +1 -1
  45. data/lib/mihari.rb +21 -0
  46. data/mihari.gemspec +13 -20
  47. metadata +51 -244
  48. data/lib/mihari/analyzers/clients/otx.rb +0 -36
@@ -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
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Clients
5
+ class ZoomEye < Base
6
+ attr_reader :api_key
7
+
8
+ def initialize(base_url = "https://api.zoomeye.org", api_key:, headers: {})
9
+ raise(ArgumentError, "'api_key' argument is required") unless api_key
10
+
11
+ headers["api-key"] = api_key
12
+ super(base_url, headers: headers)
13
+ end
14
+
15
+ #
16
+ # Search the Host devices
17
+ #
18
+ # @param [String] query Query string
19
+ # @param [Integer, nil] page The page number to paging(default:1)
20
+ # @param [String, nil] facets A comma-separated list of properties to get summary information on query
21
+ #
22
+ # @return [Hash]
23
+ #
24
+ def host_search(query, page: nil, facets: nil)
25
+ params = {
26
+ query: query,
27
+ page: page,
28
+ facets: facets
29
+ }.compact
30
+
31
+ _get("/host/search", params: params)
32
+ end
33
+
34
+ #
35
+ # Search the Web technologies
36
+ #
37
+ # @param [String] query Query string
38
+ # @param [Integer, nil] page The page number to paging(default:1)
39
+ # @param [String, nil] facets A comma-separated list of properties to get summary information on query
40
+ #
41
+ # @return [Hash]
42
+ #
43
+ def web_search(query, page: nil, facets: nil)
44
+ params = {
45
+ query: query,
46
+ page: page,
47
+ facets: facets
48
+ }.compact
49
+
50
+ _get("/web/search", params: params)
51
+ end
52
+
53
+ private
54
+
55
+ #
56
+ #
57
+ # @param [String] path
58
+ # @param [Hash] params
59
+ #
60
+ def _get(path, params: {})
61
+ res = get(path, params: params)
62
+ JSON.parse(res.body.to_s)
63
+ rescue HTTPError
64
+ nil
65
+ end
66
+ end
67
+ end
68
+ end