mihari 5.3.2 → 5.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -19,7 +19,7 @@
19
19
  "@fortawesome/vue-fontawesome": "^3.0.3",
20
20
  "@vueuse/core": "^10.3.0",
21
21
  "@vueuse/router": "^10.3.0",
22
- "ace-builds": "^1.23.4",
22
+ "ace-builds": "^1.24.0",
23
23
  "axios": "^1.4.0",
24
24
  "bulma": "^0.9.4",
25
25
  "bulma-helpers": "^0.4.3",
@@ -37,14 +37,14 @@
37
37
  "vue3-ace-editor": "^2.2.3"
38
38
  },
39
39
  "devDependencies": {
40
- "@redocly/cli": "1.0.0",
41
- "@rushstack/eslint-patch": "^1.3.2",
40
+ "@redocly/cli": "1.0.2",
41
+ "@rushstack/eslint-patch": "^1.3.3",
42
42
  "@tsconfig/node20": "^20.1.1",
43
43
  "@types/jsdom": "^21.1.1",
44
- "@types/node": "^20.4.8",
44
+ "@types/node": "^20.4.9",
45
45
  "@types/url-parse": "^1.4.8",
46
- "@typescript-eslint/eslint-plugin": "^6.2.1",
47
- "@typescript-eslint/parser": "^6.2.1",
46
+ "@typescript-eslint/eslint-plugin": "^6.3.0",
47
+ "@typescript-eslint/parser": "^6.3.0",
48
48
  "@vitejs/plugin-vue": "^4.2.3",
49
49
  "@vue/eslint-config-prettier": "^8.0.0",
50
50
  "@vue/eslint-config-typescript": "^11.0.3",
@@ -54,7 +54,7 @@
54
54
  "eslint-config-prettier": "^9.0.0",
55
55
  "eslint-plugin-prettier": "^5.0.0",
56
56
  "eslint-plugin-simple-import-sort": "^10.0.0",
57
- "eslint-plugin-vue": "^9.16.1",
57
+ "eslint-plugin-vue": "^9.17.0",
58
58
  "husky": "^8.0.3",
59
59
  "jsdom": "^22.1.0",
60
60
  "npm-run-all": "^4.1.5",
@@ -25,21 +25,28 @@ module Mihari
25
25
  # @return [Integer, nil]
26
26
  #
27
27
  def interval
28
- @interval = options[:interval]
28
+ @interval ||= options[:interval]
29
29
  end
30
30
 
31
31
  #
32
32
  # @return [Integer]
33
33
  #
34
34
  def retry_interval
35
- @retry_interval ||= options[:retry_interval] || DEFAULT_RETRY_INTERVAL
35
+ @retry_interval ||= options[:retry_interval] || Mihari.config.retry_interval
36
36
  end
37
37
 
38
38
  #
39
39
  # @return [Integer]
40
40
  #
41
41
  def retry_times
42
- @retry_times ||= options[:retry_times] || DEFAULT_RETRY_TIMES
42
+ @retry_times ||= options[:retry_times] || Mihari.config.retry_times
43
+ end
44
+
45
+ #
46
+ # @return [Integer]
47
+ #
48
+ def pagination_limit
49
+ @pagination_limit ||= options[:pagination_limit] || Mihari.config.pagination_limit
43
50
  end
44
51
 
45
52
  # @return [Array<String>, Array<Mihari::Artifact>]
@@ -56,7 +56,7 @@ module Mihari
56
56
  #
57
57
  def search
58
58
  responses = []
59
- (1..500).each do |page|
59
+ (1..pagination_limit).each do |page|
60
60
  res = search_with_page(page: page)
61
61
  total = res["total"].to_i
62
62
 
@@ -30,7 +30,7 @@ module Mihari
30
30
  artifacts = []
31
31
 
32
32
  cursor = nil
33
- loop do
33
+ pagination_limit.times do
34
34
  response = client.search(query, cursor: cursor)
35
35
  artifacts << response.result.to_artifacts
36
36
  cursor = response.result.links.next
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Analyzers
5
+ class HunterHow < Base
6
+ # @return [Integer]
7
+ PAGE_SIZE = 100
8
+
9
+ # @return [String, nil]
10
+ attr_reader :api_key
11
+
12
+ # @return [Date]
13
+ attr_reader :start_time
14
+
15
+ # @return [Date]
16
+ attr_reader :end_time
17
+
18
+ #
19
+ # @param [String] query
20
+ # @param [Hash, nil] options
21
+ # @param [String, nil] api_key
22
+ #
23
+ def initialize(query, start_time:, end_time:, options: nil, api_key: nil)
24
+ super(query, options: options)
25
+
26
+ @api_key = api_key || Mihari.config.hunterhow_api_key
27
+
28
+ @start_time = start_time
29
+ @end_time = end_time
30
+ end
31
+
32
+ #
33
+ # @return [Array<Mihari::Artifact>]
34
+ #
35
+ def artifacts
36
+ artifacts = []
37
+
38
+ (1..pagination_limit).each do |page|
39
+ res = client.search(
40
+ query,
41
+ page: page,
42
+ page_size: PAGE_SIZE,
43
+ start_time: start_time.strftime("%Y-%m-%d"),
44
+ end_time: end_time.strftime("%Y-%m-%d")
45
+ )
46
+
47
+ artifacts << res.data.artifacts
48
+
49
+ break if res.data.list.length < PAGE_SIZE
50
+
51
+ sleep_interval
52
+ end
53
+
54
+ artifacts.flatten
55
+ end
56
+
57
+ private
58
+
59
+ def configuration_keys
60
+ %w[hunterhow_api_key]
61
+ end
62
+
63
+ def client
64
+ @client ||= Clients::HunterHow.new(api_key: api_key)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -57,7 +57,7 @@ module Mihari
57
57
  #
58
58
  def search
59
59
  responses = []
60
- (1..Float::INFINITY).each do |page|
60
+ (1..pagination_limit).each do |page|
61
61
  res = search_with_page(query, page: page)
62
62
  responses << res
63
63
 
@@ -10,6 +10,7 @@ module Mihari
10
10
  "dnstwister" => DNSTwister,
11
11
  "feed" => Feed,
12
12
  "greynoise" => GreyNoise,
13
+ "hunterhow" => HunterHow,
13
14
  "onyphe" => Onyphe,
14
15
  "otx" => OTX,
15
16
  "passivetotal" => PassiveTotal,
@@ -54,7 +54,7 @@ module Mihari
54
54
  #
55
55
  def search
56
56
  responses = []
57
- (1..Float::INFINITY).each do |page|
57
+ (1..pagination_limit).each do |page|
58
58
  res = search_with_page(page: page)
59
59
  responses << res
60
60
  break if res.total <= page * PAGE_SIZE
@@ -68,7 +68,7 @@ module Mihari
68
68
  responses = []
69
69
 
70
70
  search_after = nil
71
- loop do
71
+ pagination_limit.times do
72
72
  res = search_with_search_after(search_after: search_after)
73
73
  responses << res
74
74
 
@@ -45,7 +45,7 @@ module Mihari
45
45
  cursor = nil
46
46
  responses = []
47
47
 
48
- loop do
48
+ pagination_limit.times do
49
49
  response = Structs::VirusTotalIntelligence::Response.from_dynamic!(client.intel_search(query,
50
50
  cursor: cursor))
51
51
  responses << response
@@ -95,7 +95,7 @@ module Mihari
95
95
  #
96
96
  def host_search
97
97
  responses = []
98
- (1..Float::INFINITY).each do |page|
98
+ (1..pagination_limit).each do |page|
99
99
  res = _host_search(query, page: page)
100
100
  break unless res
101
101
 
@@ -128,7 +128,7 @@ module Mihari
128
128
  #
129
129
  def web_search
130
130
  responses = []
131
- (1..Float::INFINITY).each do |page|
131
+ (1..pagination_limit).each do |page|
132
132
  res = _web_search(query, page: page)
133
133
  break unless res
134
134
 
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+
5
+ module Mihari
6
+ module Clients
7
+ class HunterHow < Base
8
+ # @return [String]
9
+ attr_reader :api_key
10
+
11
+ #
12
+ # @param [String] base_url
13
+ # @param [String, nil] api_key
14
+ # @param [Hash] headers
15
+ #
16
+ def initialize(base_url = "https://api.hunter.how/", api_key:, headers: {})
17
+ raise(ArgumentError, "'api_key' argument is required") unless api_key
18
+
19
+ super(base_url, headers: headers)
20
+
21
+ @api_key = api_key
22
+ end
23
+
24
+ #
25
+ # @param [String] query String used to query our data
26
+ # @param [Integer] page Default 1, Maximum: 500
27
+ # @param [Integer] page_size Default 100, Maximum: 100
28
+ # @param [String] start_time
29
+ # @param [String] end_time
30
+ #
31
+ # @return [Structs::HunterHow::Response]
32
+ #
33
+ def search(query, start_time:, end_time:, page: 1, page_size: 10)
34
+ params = {
35
+ query: Base64.urlsafe_encode64(query),
36
+ page: page,
37
+ page_size: page_size,
38
+ start_time: start_time,
39
+ end_time: end_time,
40
+ "api-key": api_key
41
+ }.compact
42
+ res = get("/search", params: params)
43
+ Structs::HunterHow::Response.from_dynamic! JSON.parse(res.body.to_s)
44
+ end
45
+ end
46
+ end
47
+ end
data/lib/mihari/config.rb CHANGED
@@ -1,85 +1,97 @@
1
1
  module Mihari
2
2
  class Config
3
3
  # @return [String, nil]
4
- attr_accessor :binaryedge_api_key
4
+ attr_reader :binaryedge_api_key
5
5
 
6
6
  # @return [String, nil]
7
- attr_accessor :censys_id
7
+ attr_reader :censys_id
8
8
 
9
9
  # @return [String, nil]
10
- attr_accessor :censys_secret
10
+ attr_reader :censys_secret
11
11
 
12
12
  # @return [String, nil]
13
- attr_accessor :circl_passive_password
13
+ attr_reader :circl_passive_password
14
14
 
15
15
  # @return [String, nil]
16
- attr_accessor :circl_passive_username
16
+ attr_reader :circl_passive_username
17
17
 
18
18
  # @return [URI]
19
- attr_accessor :database_url
19
+ attr_reader :database_url
20
20
 
21
21
  # @return [String, nil]
22
- attr_accessor :greynoise_api_key
22
+ attr_reader :greynoise_api_key
23
23
 
24
24
  # @return [String, nil]
25
- attr_accessor :ipinfo_api_key
25
+ attr_reader :hunterhow_api_key
26
26
 
27
27
  # @return [String, nil]
28
- attr_accessor :misp_url
28
+ attr_reader :ipinfo_api_key
29
29
 
30
30
  # @return [String, nil]
31
- attr_accessor :misp_api_key
31
+ attr_reader :misp_url
32
32
 
33
33
  # @return [String, nil]
34
- attr_accessor :onyphe_api_key
34
+ attr_reader :misp_api_key
35
35
 
36
36
  # @return [String, nil]
37
- attr_accessor :otx_api_key
37
+ attr_reader :onyphe_api_key
38
38
 
39
39
  # @return [String, nil]
40
- attr_accessor :passivetotal_api_key
40
+ attr_reader :otx_api_key
41
41
 
42
42
  # @return [String, nil]
43
- attr_accessor :passivetotal_username
43
+ attr_reader :passivetotal_api_key
44
44
 
45
45
  # @return [String, nil]
46
- attr_accessor :pulsedive_api_key
46
+ attr_reader :passivetotal_username
47
47
 
48
48
  # @return [String, nil]
49
- attr_accessor :securitytrails_api_key
49
+ attr_reader :pulsedive_api_key
50
50
 
51
51
  # @return [String, nil]
52
- attr_accessor :shodan_api_key
52
+ attr_reader :securitytrails_api_key
53
53
 
54
54
  # @return [String, nil]
55
- attr_accessor :slack_channel
55
+ attr_reader :shodan_api_key
56
56
 
57
57
  # @return [String, nil]
58
- attr_accessor :slack_webhook_url
58
+ attr_reader :slack_channel
59
59
 
60
60
  # @return [String, nil]
61
- attr_accessor :thehive_url
61
+ attr_reader :slack_webhook_url
62
62
 
63
63
  # @return [String, nil]
64
- attr_accessor :thehive_api_key
64
+ attr_reader :thehive_url
65
65
 
66
66
  # @return [String, nil]
67
- attr_accessor :thehive_api_version
67
+ attr_reader :thehive_api_key
68
68
 
69
69
  # @return [String, nil]
70
- attr_accessor :urlscan_api_key
70
+ attr_reader :thehive_api_version
71
71
 
72
72
  # @return [String, nil]
73
- attr_accessor :virustotal_api_key
73
+ attr_reader :urlscan_api_key
74
74
 
75
75
  # @return [String, nil]
76
- attr_accessor :zoomeye_api_key
76
+ attr_reader :virustotal_api_key
77
77
 
78
78
  # @return [String, nil]
79
- attr_accessor :sentry_dsn
79
+ attr_reader :zoomeye_api_key
80
80
 
81
81
  # @return [String, nil]
82
- attr_accessor :hide_config_values
82
+ attr_reader :sentry_dsn
83
+
84
+ # @return [Boolean]
85
+ attr_reader :hide_config_values
86
+
87
+ # @return [Integer]
88
+ attr_reader :retry_interval
89
+
90
+ # @return [Integer]
91
+ attr_reader :retry_times
92
+
93
+ # @return [Integer]
94
+ attr_reader :pagination_limit
83
95
 
84
96
  def initialize
85
97
  @binaryedge_api_key = ENV.fetch("BINARYEDGE_API_KEY", nil)
@@ -96,6 +108,8 @@ module Mihari
96
108
 
97
109
  @ipinfo_api_key = ENV.fetch("IPINFO_API_KEY", nil)
98
110
 
111
+ @hunterhow_api_key = ENV.fetch("HUNTERHOW_API_KEY", nil)
112
+
99
113
  @misp_url = ENV.fetch("MISP_URL", nil)
100
114
  @misp_api_key = ENV.fetch("MISP_API_KEY", nil)
101
115
 
@@ -128,6 +142,11 @@ module Mihari
128
142
  @sentry_dsn = ENV.fetch("SENTRY_DSN", nil)
129
143
 
130
144
  @hide_config_values = ENV.fetch("HIDE_CONFIG_VALUES", false)
145
+
146
+ @retry_times = ENV.fetch("RETRY_TIMES", 3).to_i
147
+ @retry_interval = ENV.fetch("RETRY_INTERVAL", 5).to_i
148
+
149
+ @pagination_limit = ENV.fetch("PAGINATION_LIMIT", 1000).to_i
131
150
  end
132
151
  end
133
152
  end
@@ -9,7 +9,4 @@ module Mihari
9
9
 
10
10
  # @return [Array<Hash>]
11
11
  DEFAULT_ENRICHERS = %w[whois ipinfo shodan google_public_dns].map { |name| { enricher: name } }.freeze
12
-
13
- DEFAULT_RETRY_TIMES = 3
14
- DEFAULT_RETRY_INTERVAL = 5
15
12
  end
@@ -4,8 +4,9 @@ module Mihari
4
4
  module Schemas
5
5
  AnalyzerOptions = Dry::Schema.Params do
6
6
  optional(:interval).value(:integer)
7
- optional(:retry_times).value(:integer).default(DEFAULT_RETRY_TIMES)
8
- optional(:retry_interval).value(:integer).default(DEFAULT_RETRY_INTERVAL)
7
+ optional(:pagination_limit).value(:integer).default(Mihari.config.pagination_limit)
8
+ optional(:retry_times).value(:integer).default(Mihari.config.retry_times)
9
+ optional(:retry_interval).value(:integer).default(Mihari.config.retry_interval)
9
10
  end
10
11
 
11
12
  AnalyzerWithoutAPIKey = Dry::Schema.Params do
@@ -75,6 +76,15 @@ module Mihari
75
76
  optional(:options).hash(AnalyzerOptions)
76
77
  end
77
78
 
79
+ HunterHow = Dry::Schema.Params do
80
+ required(:analyzer).value(Types::String.enum("hunterhow"))
81
+ required(:query).value(:string)
82
+ required(:start_time).value(:date)
83
+ required(:end_time).value(:date)
84
+ optional(:api_key).value(:string)
85
+ optional(:options).hash(AnalyzerOptions)
86
+ end
87
+
78
88
  Feed = Dry::Schema.Params do
79
89
  required(:analyzer).value(Types::String.enum("feed"))
80
90
  required(:query).value(:string)
@@ -22,7 +22,7 @@ module Mihari
22
22
  optional(:updated_on).value(:date)
23
23
 
24
24
  required(:queries).value(:array).each do
25
- AnalyzerWithoutAPIKey | AnalyzerWithAPIKey | Censys | CIRCL | PassiveTotal | ZoomEye | Crtsh | Feed
25
+ AnalyzerWithoutAPIKey | AnalyzerWithAPIKey | Censys | CIRCL | PassiveTotal | ZoomEye | Crtsh | Feed | HunterHow
26
26
  end
27
27
 
28
28
  optional(:emitters).value(:array).each { Database | MISP | TheHive | Slack | Webhook }.default(DEFAULT_EMITTERS)
@@ -0,0 +1,104 @@
1
+ module Mihari
2
+ module Structs
3
+ module HunterHow
4
+ class ListItem < Dry::Struct
5
+ attribute :domain, Types::String
6
+ attribute :ip, Types::String
7
+ attribute :port, Types::Integer
8
+
9
+ #
10
+ # @return [String]
11
+ #
12
+ def ip
13
+ attributes[:ip]
14
+ end
15
+
16
+ #
17
+ # @return [Mihari::Artifact]
18
+ #
19
+ def artifact
20
+ Artifact.new(data: ip)
21
+ end
22
+
23
+ class << self
24
+ #
25
+ # @param [Hash] d
26
+ #
27
+ # @return [ListItem]
28
+ #
29
+ def from_dynamic!(d)
30
+ d = Types::Hash[d]
31
+ new(
32
+ domain: d.fetch("domain"),
33
+ ip: d.fetch("ip"),
34
+ port: d.fetch("port")
35
+ )
36
+ end
37
+ end
38
+ end
39
+
40
+ class DataClass < Dry::Struct
41
+ attribute :list, Types.Array(ListItem)
42
+ attribute :total, Types::Integer
43
+
44
+ #
45
+ # @return [Array<ListItem>]
46
+ #
47
+ def list
48
+ attributes[:list]
49
+ end
50
+
51
+ #
52
+ # @return [Array<Mihari::Artifact>]
53
+ #
54
+ def artifacts
55
+ list.map(&:artifact)
56
+ end
57
+
58
+ class << self
59
+ #
60
+ # @param [Hash] d
61
+ #
62
+ # @return [DataClass]
63
+ #
64
+ def from_dynamic!(d)
65
+ d = Types::Hash[d]
66
+ new(
67
+ list: d.fetch("list").map { |x| ListItem.from_dynamic!(x) },
68
+ total: d.fetch("total")
69
+ )
70
+ end
71
+ end
72
+ end
73
+
74
+ class Response < Dry::Struct
75
+ attribute :code, Types::Integer
76
+ attribute :data, DataClass
77
+ attribute :message, Types::String
78
+
79
+ #
80
+ # @return [DataClass]
81
+ #
82
+ def data
83
+ attributes[:data]
84
+ end
85
+
86
+ class << self
87
+ #
88
+ # @param [Hash] d
89
+ #
90
+ # @return [Response]
91
+ #
92
+ def from_dynamic!(d)
93
+ d = Types::Hash[d]
94
+ new(
95
+ code: d.fetch("code"),
96
+ data: DataClass.from_dynamic!(d.fetch("data")),
97
+ message: d.fetch("message")
98
+ )
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mihari
4
- VERSION = "5.3.2"
4
+ VERSION = "5.4.0"
5
5
  end