mihari 0.17.5 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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:)