mihari 5.3.2 → 5.4.0

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