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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -1
  3. data/docker/Dockerfile +1 -1
  4. data/lib/mihari/analyzers/binaryedge.rb +9 -7
  5. data/lib/mihari/analyzers/censys.rb +3 -5
  6. data/lib/mihari/analyzers/circl.rb +4 -6
  7. data/lib/mihari/analyzers/crtsh.rb +6 -7
  8. data/lib/mihari/analyzers/dnpedia.rb +3 -7
  9. data/lib/mihari/analyzers/dnstwister.rb +3 -5
  10. data/lib/mihari/analyzers/feed.rb +12 -10
  11. data/lib/mihari/analyzers/greynoise.rb +3 -5
  12. data/lib/mihari/analyzers/onyphe.rb +3 -4
  13. data/lib/mihari/analyzers/otx.rb +1 -3
  14. data/lib/mihari/analyzers/passivetotal.rb +5 -7
  15. data/lib/mihari/analyzers/pulsedive.rb +5 -7
  16. data/lib/mihari/analyzers/shodan.rb +3 -9
  17. data/lib/mihari/analyzers/urlscan.rb +7 -6
  18. data/lib/mihari/analyzers/virustotal.rb +4 -6
  19. data/lib/mihari/analyzers/virustotal_intelligence.rb +4 -5
  20. data/lib/mihari/analyzers/zoomeye.rb +4 -10
  21. data/lib/mihari/cli/database.rb +11 -0
  22. data/lib/mihari/cli/main.rb +10 -4
  23. data/lib/mihari/cli/rule.rb +11 -0
  24. data/lib/mihari/clients/base.rb +53 -0
  25. data/lib/mihari/clients/binaryedge.rb +33 -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/dnpedia.rb +64 -0
  30. data/lib/mihari/clients/dnstwister.rb +40 -0
  31. data/lib/mihari/clients/greynoise.rb +29 -0
  32. data/lib/mihari/clients/misp.rb +24 -0
  33. data/lib/mihari/clients/onyphe.rb +23 -0
  34. data/lib/mihari/clients/otx.rb +29 -0
  35. data/lib/mihari/clients/passivetotal.rb +65 -0
  36. data/lib/mihari/clients/publsedive.rb +39 -0
  37. data/lib/mihari/clients/shodan.rb +30 -0
  38. data/lib/mihari/clients/the_hive.rb +28 -0
  39. data/lib/mihari/clients/urlscan.rb +31 -0
  40. data/lib/mihari/clients/virustotal.rb +56 -0
  41. data/lib/mihari/clients/zoomeye.rb +68 -0
  42. data/lib/mihari/commands/database.rb +28 -0
  43. data/lib/mihari/commands/{initializer.rb → rule.rb} +27 -6
  44. data/lib/mihari/commands/searcher.rb +5 -0
  45. data/lib/mihari/database.rb +8 -22
  46. data/lib/mihari/emitters/misp.rb +13 -20
  47. data/lib/mihari/emitters/the_hive.rb +3 -5
  48. data/lib/mihari/emitters/webhook.rb +2 -2
  49. data/lib/mihari/feed/reader.rb +14 -11
  50. data/lib/mihari/http.rb +29 -21
  51. data/lib/mihari/mixins/database.rb +2 -0
  52. data/lib/mihari/mixins/retriable.rb +3 -1
  53. data/lib/mihari/schemas/analyzer.rb +5 -4
  54. data/lib/mihari/version.rb +1 -1
  55. data/lib/mihari.rb +21 -0
  56. data/mihari.gemspec +14 -20
  57. metadata +61 -238
  58. data/lib/mihari/analyzers/clients/otx.rb +0 -36
  59. 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