mihari 5.1.0 → 5.1.2

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.
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