mihari 5.1.0 → 5.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.gitmodules +0 -3
  3. data/.rubocop.yml +6 -0
  4. data/README.md +0 -1
  5. data/lib/mihari/analyzers/base.rb +32 -27
  6. data/lib/mihari/analyzers/binaryedge.rb +17 -9
  7. data/lib/mihari/analyzers/censys.rb +10 -54
  8. data/lib/mihari/analyzers/circl.rb +7 -6
  9. data/lib/mihari/analyzers/crtsh.rb +12 -7
  10. data/lib/mihari/analyzers/dnstwister.rb +7 -7
  11. data/lib/mihari/analyzers/feed.rb +33 -10
  12. data/lib/mihari/analyzers/greynoise.rb +8 -33
  13. data/lib/mihari/analyzers/onyphe.rb +10 -36
  14. data/lib/mihari/analyzers/otx.rb +4 -3
  15. data/lib/mihari/analyzers/passivetotal.rb +8 -7
  16. data/lib/mihari/analyzers/pulsedive.rb +8 -7
  17. data/lib/mihari/analyzers/rule.rb +0 -1
  18. data/lib/mihari/analyzers/securitytrails.rb +8 -10
  19. data/lib/mihari/analyzers/shodan.rb +16 -90
  20. data/lib/mihari/analyzers/urlscan.rb +16 -6
  21. data/lib/mihari/analyzers/virustotal.rb +8 -6
  22. data/lib/mihari/analyzers/virustotal_intelligence.rb +12 -7
  23. data/lib/mihari/analyzers/zoomeye.rb +13 -10
  24. data/lib/mihari/clients/base.rb +53 -0
  25. data/lib/mihari/clients/binaryedge.rb +38 -0
  26. data/lib/mihari/clients/censys.rb +42 -0
  27. data/lib/mihari/clients/circl.rb +59 -0
  28. data/lib/mihari/clients/crtsh.rb +31 -0
  29. data/lib/mihari/clients/dnstwister.rb +40 -0
  30. data/lib/mihari/clients/greynoise.rb +34 -0
  31. data/lib/mihari/clients/misp.rb +29 -0
  32. data/lib/mihari/clients/onyphe.rb +35 -0
  33. data/lib/mihari/clients/otx.rb +49 -0
  34. data/lib/mihari/clients/passivetotal.rb +69 -0
  35. data/lib/mihari/clients/publsedive.rb +56 -0
  36. data/lib/mihari/clients/securitytrails.rb +94 -0
  37. data/lib/mihari/clients/shodan.rb +41 -0
  38. data/lib/mihari/clients/the_hive.rb +33 -0
  39. data/lib/mihari/clients/urlscan.rb +33 -0
  40. data/lib/mihari/clients/virustotal.rb +62 -0
  41. data/lib/mihari/clients/zoomeye.rb +74 -0
  42. data/lib/mihari/commands/database.rb +1 -6
  43. data/lib/mihari/commands/searcher.rb +1 -2
  44. data/lib/mihari/database.rb +9 -0
  45. data/lib/mihari/emitters/misp.rb +13 -20
  46. data/lib/mihari/emitters/the_hive.rb +3 -5
  47. data/lib/mihari/emitters/webhook.rb +2 -2
  48. data/lib/mihari/feed/reader.rb +14 -11
  49. data/lib/mihari/http.rb +29 -21
  50. data/lib/mihari/mixins/retriable.rb +3 -1
  51. data/lib/mihari/schemas/analyzer.rb +5 -4
  52. data/lib/mihari/structs/censys.rb +62 -0
  53. data/lib/mihari/structs/greynoise.rb +43 -0
  54. data/lib/mihari/structs/onyphe.rb +45 -0
  55. data/lib/mihari/structs/shodan.rb +83 -0
  56. data/lib/mihari/version.rb +1 -1
  57. data/lib/mihari/web/middleware/connection_adapter.rb +1 -3
  58. data/lib/mihari/web/public/assets/{index-63900d73.js → index-7d0fb8c4.js} +2 -2
  59. data/lib/mihari/web/public/index.html +1 -1
  60. data/lib/mihari/web/public/redoc-static.html +2 -2
  61. data/lib/mihari.rb +21 -2
  62. data/mihari.gemspec +15 -23
  63. metadata +55 -264
  64. data/lib/mihari/analyzers/clients/otx.rb +0 -36
  65. data/lib/mihari/analyzers/dnpedia.rb +0 -37
  66. data/lib/mihari/mixins/database.rb +0 -16
@@ -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, nil] username
11
+ # @param [String, nil] 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 [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,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,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Clients
5
+ class GreyNoise < Base
6
+ #
7
+ # @param [String] base_url
8
+ # @param [String, nil] api_key
9
+ # @param [Hash] headers
10
+ #
11
+ def initialize(base_url = "https://api.greynoise.io", api_key:, headers: {})
12
+ raise(ArgumentError, "'api_key' argument is required") unless api_key
13
+
14
+ headers["key"] = api_key
15
+ super(base_url, headers: headers)
16
+ end
17
+
18
+ #
19
+ # GNQL (GreyNoise Query Language) is a domain-specific query language that uses Lucene deep under the hood
20
+ #
21
+ # @param [String] query GNQL query string
22
+ # @param [Integer, nil] size Maximum amount of results to grab
23
+ # @param [Integer, nil] scroll Scroll token to paginate through results
24
+ #
25
+ # @return [Hash]
26
+ #
27
+ def gnql_search(query, size: nil, scroll: nil)
28
+ params = { query: query, size: size, scroll: scroll }.compact
29
+ res = get("/v2/experimental/gnql", params: params)
30
+ Structs::GreyNoise::Response.from_dynamic! JSON.parse(res.body.to_s)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,29 @@
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, nil] 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
+ #
19
+ # @param [Hash] payload
20
+ #
21
+ # @return [Hash]
22
+ #
23
+ def create_event(payload)
24
+ res = post("/events/add", json: payload)
25
+ JSON.parse(res.body.to_s)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Clients
5
+ class Onyphe < Base
6
+ # @return [String]
7
+ attr_reader :api_key
8
+
9
+ #
10
+ # @param [String] base_url
11
+ # @param [String, nil] api_key
12
+ # @param [Hash] headers
13
+ #
14
+ def initialize(base_url = "https://www.onyphe.io", api_key:, headers: {})
15
+ raise(ArgumentError, "'api_key' argument is required") if api_key.nil?
16
+
17
+ super(base_url, headers: headers)
18
+
19
+ @api_key = api_key
20
+ end
21
+
22
+ #
23
+ # @param [String] query
24
+ # @param [Integer] page
25
+ #
26
+ # @return [Hash]
27
+ #
28
+ def datascan(query, page: 1)
29
+ params = { page: page, apikey: api_key }
30
+ res = get("/api/v2/simple/datascan/#{query}", params: params)
31
+ Structs::Onyphe::Response.from_dynamic! JSON.parse(res.body.to_s)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Clients
5
+ class OTX < Base
6
+ #
7
+ # @param [String] base_url
8
+ # @param [String, nil] api_key
9
+ # @param [Hash] headers
10
+ #
11
+ def initialize(base_url = "https://otx.alienvault.com", api_key:, headers: {})
12
+ raise(ArgumentError, "'api_key' argument is required") unless api_key
13
+
14
+ headers["x-otx-api-key"] = api_key
15
+ super(base_url, headers: headers)
16
+ end
17
+
18
+ #
19
+ # @param [String] ip
20
+ #
21
+ # @return [Hash]
22
+ #
23
+ def query_by_ip(ip)
24
+ _get "/api/v1/indicators/IPv4/#{ip}/passive_dns"
25
+ end
26
+
27
+ #
28
+ # @param [String] domain
29
+ #
30
+ # @return [Hash]
31
+ #
32
+ def query_by_domain(domain)
33
+ _get "/api/v1/indicators/domain/#{domain}/passive_dns"
34
+ end
35
+
36
+ private
37
+
38
+ #
39
+ # @param [String] path
40
+ #
41
+ # @return [Hash]
42
+ #
43
+ def _get(path)
44
+ res = get(path)
45
+ JSON.parse(res.body.to_s)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,69 @@
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, nil] username
11
+ # @param [String, nil] 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
+ # @return [Hash]
35
+ #
36
+ def passive_dns_search(query)
37
+ params = { query: query }
38
+ _get("/v2/dns/passive/unique", params: params)
39
+ end
40
+
41
+ #
42
+ # @param [String] query the domain being queried
43
+ # @param [String] field whether to return historical results
44
+ #
45
+ # @return [Hash]
46
+ #
47
+ def reverse_whois_search(query:, field:)
48
+ params = {
49
+ query: query,
50
+ field: field
51
+ }.compact
52
+ _get("/v2/whois/search", params: params)
53
+ end
54
+
55
+ private
56
+
57
+ #
58
+ # @param [String] path
59
+ # @param [Hash] params
60
+ #
61
+ # @return [Hash]
62
+ #
63
+ def _get(path, params: {})
64
+ res = get(path, params: params)
65
+ JSON.parse(res.body.to_s)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Clients
5
+ class PulseDive < Base
6
+ # @return [String]
7
+ attr_reader :api_key
8
+
9
+ #
10
+ # @param [String] base_url
11
+ # @param [String, nil] api_key
12
+ # @param [Hash] headers
13
+ #
14
+ def initialize(base_url = "https://pulsedive.com", api_key:, headers: {})
15
+ super(base_url, headers: headers)
16
+
17
+ @api_key = api_key
18
+
19
+ raise(ArgumentError, "'api_key' argument is required") unless api_key
20
+ end
21
+
22
+ #
23
+ # @param [String] indicator_id
24
+ #
25
+ # @return [Hash]
26
+ #
27
+ def get_indicator(ip_or_domain)
28
+ _get "/api/info.php", params: { indicator: ip_or_domain }
29
+ end
30
+
31
+ #
32
+ # @param [String] indicator_id
33
+ #
34
+ # @return [Hash]
35
+ #
36
+ def get_properties(indicator_id)
37
+ _get "/api/info.php", params: { iid: indicator_id, get: "properties" }
38
+ end
39
+
40
+ private
41
+
42
+ #
43
+ # @param [String] path
44
+ # @param [Hash] params
45
+ #
46
+ # @return [Hash]
47
+ #
48
+ def _get(path, params: {})
49
+ params["key"] = api_key
50
+
51
+ res = get(path, params: params)
52
+ JSON.parse(res.body.to_s)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Clients
5
+ class SecurityTrails < Base
6
+ #
7
+ # @param [String] base_url
8
+ # @param [String, nil] api_key
9
+ # @param [Hash] headers
10
+ #
11
+ def initialize(base_url = "https://api.securitytrails.com", api_key:, headers: {})
12
+ raise(ArgumentError, "'api_key' argument is required") unless api_key
13
+
14
+ headers["apikey"] = api_key
15
+ super(base_url, headers: headers)
16
+ end
17
+
18
+ #
19
+ # @param [String] mail
20
+ #
21
+ # @return [Array<Hash>]
22
+ #
23
+ def search_by_mail(mail)
24
+ res = _post "/v1/domains/list", json: { filter: { whois_email: mail } }
25
+ res["records"] || []
26
+ end
27
+
28
+ #
29
+ # @param [String] ip
30
+ #
31
+ # @return [Array<Hash>]
32
+ #
33
+ def search_by_ip(ip)
34
+ res = _post "/v1/domains/list", json: { filter: { ipv4: ip } }
35
+ res["records"] || []
36
+ end
37
+
38
+ #
39
+ # @param [String] domain
40
+ # @param [String] type
41
+ #
42
+ # @return [Array<Hash>]
43
+ #
44
+ def get_all_dns_history(domain, type:)
45
+ first_page = get_dns_history(domain, type: type, page: 1)
46
+
47
+ pages = first_page["pages"].to_i
48
+ records = first_page["records"] || []
49
+
50
+ (2..pages).each do |page_idx|
51
+ next_page = get_dns_history(domain, type: type, page: page_idx)
52
+ records << next_page["records"]
53
+ end
54
+
55
+ records.flatten
56
+ end
57
+
58
+ private
59
+
60
+ #
61
+ # @param [String] domain
62
+ # @param [String] type
63
+ # @param [Integer] page
64
+ #
65
+ # @return [Array<Hash>]
66
+ #
67
+ def get_dns_history(domain, type:, page:)
68
+ _get "/v1/history/#{domain}/dns/#{type}", params: { page: page }
69
+ end
70
+
71
+ #
72
+ # @param [String] path
73
+ # @param [Hash, nil] params
74
+ #
75
+ # @return [Hash]
76
+ #
77
+ def _get(path, params:)
78
+ res = get(path, params: params)
79
+ JSON.parse(res.body.to_s)
80
+ end
81
+
82
+ #
83
+ # @param [String] path
84
+ # @param [Hash, nil] json
85
+ #
86
+ # @return [Hash]
87
+ #
88
+ def _post(path, json:)
89
+ res = post(path, json: json)
90
+ JSON.parse(res.body.to_s)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Clients
5
+ class Shodan < Base
6
+ # @return [String]
7
+ attr_reader :api_key
8
+
9
+ #
10
+ # @param [String] base_url
11
+ # @param [String, nil] api_key
12
+ # @param [Hash] headers
13
+ #
14
+ def initialize(base_url = "https://api.shodan.io", api_key:, headers: {})
15
+ raise(ArgumentError, "'api_key' argument is required") unless api_key
16
+
17
+ super(base_url, headers: headers)
18
+
19
+ @api_key = api_key
20
+ end
21
+
22
+ #
23
+ # @param [String] query
24
+ # @param [Integer] page
25
+ # @param [Boolean] minify
26
+ #
27
+ # @return [Structs::Shodan::Result]
28
+ #
29
+ def search(query, page: 1, minify: true)
30
+ params = {
31
+ query: query,
32
+ page: page,
33
+ minify: minify,
34
+ key: api_key
35
+ }
36
+ res = get("/shodan/host/search", params: params)
37
+ Structs::Shodan::Result.from_dynamic! JSON.parse(res.body.to_s)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,33 @@
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, nil] 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
+ #
22
+ # @param [Hash] json
23
+ #
24
+ # @return [Hash]
25
+ #
26
+ def alert(json)
27
+ json = json.to_camelback_keys.compact
28
+ res = post("/alert", json: json)
29
+ JSON.parse(res.body.to_s)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,33 @@
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, nil] 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
+ # @return [Hash]
25
+ #
26
+ def search(q, size: 100, search_after: nil)
27
+ params = { q: q, size: size, search_after: search_after }.compact
28
+ res = get("/api/v1/search/", params: params)
29
+ JSON.parse res.body.to_s
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,62 @@
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, nil] api_key
9
+ # @param [Hash] headers
10
+ #
11
+ def initialize(base_url = "https://www.virustotal.com", api_key:, headers: {})
12
+ raise(ArgumentError, "'api_key' argument is required") if api_key.nil?
13
+
14
+ headers["x-apikey"] = api_key
15
+
16
+ super(base_url, headers: headers)
17
+ end
18
+
19
+ #
20
+ # @param [String] query
21
+ #
22
+ # @return [Hash]
23
+ #
24
+ def domain_search(query)
25
+ _get("/api/v3/domains/#{query}/resolutions")
26
+ end
27
+
28
+ #
29
+ # @param [String] query
30
+ #
31
+ # @return [Hash]
32
+ #
33
+ def ip_search(query)
34
+ _get("/api/v3/ip_addresses/#{query}/resolutions")
35
+ end
36
+
37
+ #
38
+ # @param [String] query
39
+ # @param [String, nil] cursor
40
+ #
41
+ # @return [Hash]
42
+ #
43
+ def intel_search(query, cursor: nil)
44
+ params = { query: query, cursor: cursor }.compact
45
+ _get("/api/v3/intelligence/search", params: params)
46
+ end
47
+
48
+ private
49
+
50
+ #
51
+ # @param [String] path
52
+ # @param [Hash] params
53
+ #
54
+ # @return [Hash]
55
+ #
56
+ def _get(path, params: {})
57
+ res = get(path, params: params)
58
+ JSON.parse(res.body.to_s)
59
+ end
60
+ end
61
+ end
62
+ end