mihari 0.17.5 → 1.2.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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop.yml +155 -0
  4. data/.travis.yml +7 -1
  5. data/Gemfile +2 -0
  6. data/README.md +45 -73
  7. data/config/pre_commit.yml +3 -0
  8. data/docker/Dockerfile +1 -1
  9. data/lib/mihari.rb +13 -8
  10. data/lib/mihari/alert_viewer.rb +16 -34
  11. data/lib/mihari/analyzers/base.rb +7 -19
  12. data/lib/mihari/analyzers/basic.rb +3 -1
  13. data/lib/mihari/analyzers/binaryedge.rb +2 -2
  14. data/lib/mihari/analyzers/censys.rb +2 -2
  15. data/lib/mihari/analyzers/circl.rb +2 -2
  16. data/lib/mihari/analyzers/onyphe.rb +3 -3
  17. data/lib/mihari/analyzers/otx.rb +74 -0
  18. data/lib/mihari/analyzers/passive_dns.rb +2 -1
  19. data/lib/mihari/analyzers/passivetotal.rb +2 -2
  20. data/lib/mihari/analyzers/pulsedive.rb +2 -2
  21. data/lib/mihari/analyzers/securitytrails.rb +2 -2
  22. data/lib/mihari/analyzers/securitytrails_domain_feed.rb +2 -2
  23. data/lib/mihari/analyzers/shodan.rb +2 -2
  24. data/lib/mihari/analyzers/virustotal.rb +2 -2
  25. data/lib/mihari/analyzers/zoomeye.rb +2 -2
  26. data/lib/mihari/cli.rb +23 -4
  27. data/lib/mihari/config.rb +70 -2
  28. data/lib/mihari/configurable.rb +1 -1
  29. data/lib/mihari/database.rb +68 -0
  30. data/lib/mihari/emitters/base.rb +1 -1
  31. data/lib/mihari/emitters/database.rb +29 -0
  32. data/lib/mihari/emitters/misp.rb +8 -1
  33. data/lib/mihari/emitters/slack.rb +4 -2
  34. data/lib/mihari/emitters/stdout.rb +2 -1
  35. data/lib/mihari/emitters/the_hive.rb +28 -14
  36. data/lib/mihari/models/alert.rb +11 -0
  37. data/lib/mihari/models/artifact.rb +27 -0
  38. data/lib/mihari/models/tag.rb +10 -0
  39. data/lib/mihari/models/tagging.rb +10 -0
  40. data/lib/mihari/notifiers/slack.rb +7 -4
  41. data/lib/mihari/serializers/alert.rb +12 -0
  42. data/lib/mihari/serializers/artifact.rb +9 -0
  43. data/lib/mihari/serializers/tag.rb +9 -0
  44. data/lib/mihari/slack_monkeypatch.rb +16 -0
  45. data/lib/mihari/status.rb +1 -1
  46. data/lib/mihari/type_checker.rb +1 -1
  47. data/lib/mihari/version.rb +1 -1
  48. data/mihari.gemspec +13 -5
  49. metadata +149 -30
  50. data/lib/mihari/artifact.rb +0 -36
  51. data/lib/mihari/cache.rb +0 -35
  52. data/lib/mihari/the_hive.rb +0 -42
  53. data/lib/mihari/the_hive/alert.rb +0 -25
  54. data/lib/mihari/the_hive/artifact.rb +0 -33
  55. data/lib/mihari/the_hive/base.rb +0 -14
@@ -2,40 +2,22 @@
2
2
 
3
3
  module Mihari
4
4
  class AlertViewer
5
- attr_reader :limit
6
- attr_reader :the_hive
7
-
8
- ALERT_KEYS = %w(title description artifacts tags createdAt status).freeze
9
-
10
- def initialize(limit: 5)
11
- @limit = limit
12
- validate_limit
13
-
14
- @the_hive = TheHive.new
15
- raise Error, "Cannot connect to the TheHive instance" unless the_hive.valid?
16
- end
17
-
18
- def list
19
- range = limit == "all" ? "all" : "0-#{limit}"
20
- alerts = the_hive.alert.list(range: range)
21
- alerts.map { |alert| convert alert }
22
- end
23
-
24
- private
25
-
26
- def validate_limit
27
- return true if limit == "all"
28
-
29
- raise ArgumentError, "limit should be bigger than zero" unless limit.to_i.positive?
30
- end
31
-
32
- def convert(alert)
33
- attributes = alert.select { |k, _v| ALERT_KEYS.include? k }
34
- attributes["createdAt"] = Time.at(attributes["createdAt"] / 1000).to_s
35
- attributes["artifacts"] = (attributes.dig("artifacts") || []).map do |artifact|
36
- artifact.dig("data")
37
- end.sort
38
- attributes
5
+ def list(title: nil, source: nil, tag: nil, limit: 5)
6
+ limit = limit.to_i
7
+ raise ArgumentError, "limit should be bigger than zero" unless limit.positive?
8
+
9
+ relation = Alert.includes(:tags, :artifacts)
10
+ relation = relation.where(title: title) if title
11
+ relation = relation.where(source: source) if source
12
+ relation = relation.where(tags: { name: tag } ) if tag
13
+
14
+ alerts = relation.limit(limit).order(id: :desc)
15
+ alerts.map do |alert|
16
+ json = AlertSerializer.new(alert).as_json
17
+ json[:artifacts] = (json.dig(:artifacts) || []).map { |artifact_| artifact_.dig(:data) }
18
+ json[:tags] = (json.dig(:tags) || []).map { |tag_| tag_.dig(:name) }
19
+ json
20
+ end
39
21
  end
40
22
  end
41
23
  end
@@ -23,6 +23,10 @@ module Mihari
23
23
  raise NotImplementedError, "You must implement #{self.class}##{__method__}"
24
24
  end
25
25
 
26
+ def source
27
+ self.class.to_s.split("::").last
28
+ end
29
+
26
30
  # @return [Array<String>]
27
31
  def tags
28
32
  []
@@ -37,7 +41,7 @@ module Mihari
37
41
  end
38
42
 
39
43
  def run_emitter(emitter)
40
- emitter.run(title: title, description: description, artifacts: unique_artifacts, tags: tags)
44
+ emitter.run(title: title, description: description, artifacts: unique_artifacts, source: source, tags: tags)
41
45
  rescue StandardError => e
42
46
  puts "Emission by #{emitter.class} is failed: #{e}"
43
47
  end
@@ -48,32 +52,16 @@ module Mihari
48
52
 
49
53
  private
50
54
 
51
- def the_hive
52
- @the_hive ||= TheHive.new
53
- end
54
-
55
- def cache
56
- @cache ||= Cache.new
57
- end
58
-
59
55
  # @return [Array<Mihari::Artifact>]
60
56
  def normalized_artifacts
61
57
  @normalized_artifacts ||= artifacts.compact.uniq.sort.map do |artifact|
62
- artifact.is_a?(Artifact) ? artifact : Artifact.new(artifact)
58
+ artifact.is_a?(Artifact) ? artifact : Artifact.new(data: artifact)
63
59
  end.select(&:valid?)
64
60
  end
65
61
 
66
- def uncached_artifacts
67
- @uncached_artifacts ||= normalized_artifacts.reject do |artifact|
68
- cache.cached? artifact.data
69
- end
70
- end
71
-
72
62
  # @return [Array<Mihari::Artifact>]
73
63
  def unique_artifacts
74
- return uncached_artifacts unless the_hive.valid?
75
-
76
- @unique_artifacts ||= the_hive.artifact.find_non_existing_artifacts(uncached_artifacts)
64
+ @unique_artifacts ||= normalized_artifacts.select(&:unique?)
77
65
  end
78
66
 
79
67
  def set_unique_artifacts
@@ -6,12 +6,14 @@ module Mihari
6
6
  attr_accessor :title
7
7
  attr_reader :description
8
8
  attr_reader :artifacts
9
+ attr_reader :source
9
10
  attr_reader :tags
10
11
 
11
- def initialize(title:, description:, artifacts:, tags: [])
12
+ def initialize(title:, description:, artifacts:, source:, tags: [])
12
13
  @title = title
13
14
  @description = description
14
15
  @artifacts = artifacts
16
+ @source = source
15
17
  @tags = tags
16
18
  end
17
19
  end
@@ -52,11 +52,11 @@ module Mihari
52
52
  end
53
53
 
54
54
  def config_keys
55
- %w(BINARYEDGE_API_KEY)
55
+ %w(binaryedge_api_key)
56
56
  end
57
57
 
58
58
  def api
59
- @api ||= ::BinaryEdge::API.new
59
+ @api ||= ::BinaryEdge::API.new(Mihari.config.binaryedge_api_key)
60
60
  end
61
61
  end
62
62
  end
@@ -86,11 +86,11 @@ module Mihari
86
86
  end
87
87
 
88
88
  def config_keys
89
- %w(CENSYS_ID CENSYS_SECRET)
89
+ %w(censys_id censys_secret)
90
90
  end
91
91
 
92
92
  def api
93
- @api ||= ::Censys::API.new
93
+ @api ||= ::Censys::API.new(Mihari.config.censys_id, Mihari.config.censys_secret)
94
94
  end
95
95
  end
96
96
  end
@@ -27,11 +27,11 @@ module Mihari
27
27
  private
28
28
 
29
29
  def config_keys
30
- %w(CIRCL_PASSIVE_USERNAME CIRCL_PASSIVE_PASSWORD)
30
+ %w(circl_passive_password circl_passive_username)
31
31
  end
32
32
 
33
33
  def api
34
- @api ||= ::PassiveCIRCL::API.new
34
+ @api ||= ::PassiveCIRCL::API.new(username: Mihari.config.circl_passive_username, password: Mihari.config.circl_passive_password)
35
35
  end
36
36
 
37
37
  def lookup
@@ -35,15 +35,15 @@ module Mihari
35
35
  PAGE_SIZE = 10
36
36
 
37
37
  def config_keys
38
- %w(ONYPHE_API_KEY)
38
+ %w(onyphe_api_key)
39
39
  end
40
40
 
41
41
  def api
42
- @api ||= ::Onyphe::API.new
42
+ @api ||= ::Onyphe::API.new(Mihari.config.onyphe_api_key)
43
43
  end
44
44
 
45
45
  def search_with_page(query, page: 1)
46
- api.datascan(query, page: page)
46
+ api.simple.datascan(query, page: page)
47
47
  end
48
48
 
49
49
  def search
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "otx_ruby"
4
+
5
+ module Mihari
6
+ module Analyzers
7
+ class OTX < Base
8
+ attr_reader :query
9
+ attr_reader :type
10
+
11
+ attr_reader :title
12
+ attr_reader :description
13
+ attr_reader :tags
14
+
15
+ def initialize(query, title: nil, description: nil, tags: [])
16
+ super()
17
+
18
+ @query = query
19
+ @type = TypeChecker.type(query)
20
+
21
+ @title = title || "OTX lookup"
22
+ @description = description || "query = #{query}"
23
+ @tags = tags
24
+ end
25
+
26
+ def artifacts
27
+ lookup || []
28
+ end
29
+
30
+ private
31
+
32
+ def config_keys
33
+ %w(otx_api_key)
34
+ end
35
+
36
+ def domain_client
37
+ @domain_client ||= ::OTX::Domain.new(Mihari.config.otx_api_key)
38
+ end
39
+
40
+ def ip_client
41
+ @ip_client ||= ::OTX::IP.new(Mihari.config.otx_api_key)
42
+ end
43
+
44
+ def valid_type?
45
+ %w(ip domain).include? type
46
+ end
47
+
48
+ def lookup
49
+ case type
50
+ when "domain"
51
+ domain_lookup
52
+ when "ip"
53
+ ip_lookup
54
+ else
55
+ raise InvalidInputError, "#{query}(type: #{type || 'unknown'}) is not supported." unless valid_type?
56
+ end
57
+ end
58
+
59
+ def domain_lookup
60
+ records = domain_client.get_passive_dns(query)
61
+ records.map do |record|
62
+ record.address if record.record_type == "A"
63
+ end.compact.uniq
64
+ end
65
+
66
+ def ip_lookup
67
+ records = ip_client.get_passive_dns(query)
68
+ records.map do |record|
69
+ record.hostname if record.record_type == "A"
70
+ end.compact.uniq
71
+ end
72
+ end
73
+ end
74
+ end
@@ -14,6 +14,7 @@ module Mihari
14
14
 
15
15
  ANALYZERS = [
16
16
  Mihari::Analyzers::CIRCL,
17
+ Mihari::Analyzers::OTX,
17
18
  Mihari::Analyzers::PassiveTotal,
18
19
  Mihari::Analyzers::Pulsedive,
19
20
  Mihari::Analyzers::SecurityTrails,
@@ -55,7 +56,7 @@ module Mihari
55
56
  analyzer.artifacts
56
57
  rescue ArgumentError, InvalidInputError => _e
57
58
  nil
58
- rescue ::PassiveCIRCL::Error, ::PassiveTotal::Error, ::Pulsedive::ResponseError, ::SecurityTrails::Error, ::VirusTotal::Error => _e
59
+ rescue Faraday::Error, ::PassiveCIRCL::Error, ::PassiveTotal::Error, ::Pulsedive::ResponseError, ::SecurityTrails::Error, ::VirusTotal::Error => _e
59
60
  nil
60
61
  end
61
62
  end
@@ -30,11 +30,11 @@ module Mihari
30
30
  private
31
31
 
32
32
  def config_keys
33
- %w(PASSIVETOTAL_USERNAME PASSIVETOTAL_API_KEY)
33
+ %w(passivetotal_username passivetotal_api_key)
34
34
  end
35
35
 
36
36
  def api
37
- @api ||= ::PassiveTotal::API.new
37
+ @api ||= ::PassiveTotal::API.new(username: Mihari.config.passivetotal_username, api_key: Mihari.config.passivetotal_api_key)
38
38
  end
39
39
 
40
40
  def valid_type?
@@ -30,11 +30,11 @@ module Mihari
30
30
  private
31
31
 
32
32
  def config_keys
33
- %w(PULSEDIVE_API_KEY)
33
+ %w(pulsedive_api_key)
34
34
  end
35
35
 
36
36
  def api
37
- @api ||= ::Pulsedive::API.new
37
+ @api ||= ::Pulsedive::API.new(Mihari.config.pulsedive_api_key)
38
38
  end
39
39
 
40
40
  def valid_type?
@@ -30,11 +30,11 @@ module Mihari
30
30
  private
31
31
 
32
32
  def config_keys
33
- %w(SECURITYTRAILS_API_KEY)
33
+ %w(securitytrails_api_key)
34
34
  end
35
35
 
36
36
  def api
37
- @api ||= ::SecurityTrails::API.new
37
+ @api ||= ::SecurityTrails::API.new(Mihari.config.securitytrails_api_key)
38
38
  end
39
39
 
40
40
  def valid_type?
@@ -32,11 +32,11 @@ module Mihari
32
32
  private
33
33
 
34
34
  def config_keys
35
- %w(SECURITYTRAILS_API_KEY)
35
+ %w(securitytrails_api_key)
36
36
  end
37
37
 
38
38
  def api
39
- @api ||= ::SecurityTrails::API.new
39
+ @api ||= ::SecurityTrails::API.new(Mihari.config.securitytrails_api_key)
40
40
  end
41
41
 
42
42
  def valid_type?
@@ -36,11 +36,11 @@ module Mihari
36
36
  PAGE_SIZE = 100
37
37
 
38
38
  def config_keys
39
- %w(SHODAN_API_KEY)
39
+ %w(shodan_api_key)
40
40
  end
41
41
 
42
42
  def api
43
- @api ||= ::Shodan::API.new
43
+ @api ||= ::Shodan::API.new(key: Mihari.config.shodan_api_key)
44
44
  end
45
45
 
46
46
  def search_with_page(query, page: 1)
@@ -30,11 +30,11 @@ module Mihari
30
30
  private
31
31
 
32
32
  def config_keys
33
- %w(VIRUSTOTAL_API_KEY)
33
+ %w(virustotal_api_key)
34
34
  end
35
35
 
36
36
  def api
37
- @api = ::VirusTotal::API.new
37
+ @api = ::VirusTotal::API.new(key: Mihari.config.virustotal_api_key)
38
38
  end
39
39
 
40
40
  def valid_type?
@@ -41,11 +41,11 @@ module Mihari
41
41
  end
42
42
 
43
43
  def config_keys
44
- %w(ZOOMEYE_USERNAME ZOOMEYE_PASSWORD)
44
+ %w(zoomeye_password zoomeye_username)
45
45
  end
46
46
 
47
47
  def api
48
- @api ||= ::ZoomEye::API.new
48
+ @api ||= ::ZoomEye::API.new(username: Mihari.config.zoomeye_username, password: Mihari.config.zoomeye_password)
49
49
  end
50
50
 
51
51
  def convert_responses(responses)
@@ -164,6 +164,16 @@ module Mihari
164
164
  end
165
165
  end
166
166
 
167
+ desc "otx [IP|DOMAIN]", "OTX lookup by an IP or domain"
168
+ method_option :title, type: :string, desc: "title"
169
+ method_option :description, type: :string, desc: "description"
170
+ method_option :tags, type: :array, desc: "tags"
171
+ def otx(domain)
172
+ with_error_handling do
173
+ run_analyzer Analyzers::OTX, query: refang(domain), options: options
174
+ end
175
+ end
176
+
167
177
  desc "passive_dns [IP|DOMAIN]", "Cross search with passive DNS services by an ip or domain"
168
178
  method_option :title, type: :string, desc: "title"
169
179
  method_option :description, type: :string, desc: "description"
@@ -242,17 +252,22 @@ module Mihari
242
252
  artifacts = json.dig("artifacts")
243
253
  tags = json.dig("tags") || []
244
254
 
245
- basic = Analyzers::Basic.new(title: title, description: description, artifacts: artifacts, tags: tags)
255
+ basic = Analyzers::Basic.new(title: title, description: description, artifacts: artifacts, source: "json", tags: tags)
246
256
  basic.run
247
257
  end
248
258
  end
249
259
 
250
260
  desc "alerts", "Show the alerts on TheHive"
251
261
  method_option :limit, type: :string, default: "5", desc: "Number of alerts to show (or 'all' to show all the alerts)"
262
+ method_option :title, type: :string, desc: "Title to filter"
263
+ method_option :source, type: :string, desc: "Source to filter"
264
+ method_option :tag, type: :string, desc: "Tag to filter"
252
265
  def alerts
253
266
  with_error_handling do
254
- viewer = AlertViewer.new(limit: options["limit"])
255
- alerts = viewer.list
267
+ load_configuration
268
+
269
+ viewer = AlertViewer.new
270
+ alerts = viewer.list(limit: options["limit"], title: options["title"], source: options["source"], tag: options[:tag])
256
271
  puts JSON.pretty_generate(alerts)
257
272
  end
258
273
  end
@@ -261,6 +276,7 @@ module Mihari
261
276
  def status
262
277
  with_error_handling do
263
278
  load_configuration
279
+
264
280
  puts JSON.pretty_generate(Status.check)
265
281
  end
266
282
  end
@@ -286,7 +302,10 @@ module Mihari
286
302
 
287
303
  def load_configuration
288
304
  config = options["config"]
289
- Config.load_from_yaml(config) if config
305
+ return unless config
306
+
307
+ Config.load_from_yaml(config)
308
+ Database.connect
290
309
  end
291
310
 
292
311
  def run_analyzer(analyzer_class, query:, options:)