mihari 3.3.0 → 3.6.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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -0
  3. data/config.ru +1 -0
  4. data/lib/mihari/analyzers/base.rb +34 -6
  5. data/lib/mihari/analyzers/censys.rb +37 -9
  6. data/lib/mihari/analyzers/onyphe.rb +34 -9
  7. data/lib/mihari/analyzers/shodan.rb +26 -5
  8. data/lib/mihari/cli/analyzer.rb +4 -0
  9. data/lib/mihari/cli/base.rb +0 -5
  10. data/lib/mihari/commands/init.rb +4 -4
  11. data/lib/mihari/commands/search.rb +17 -8
  12. data/lib/mihari/commands/web.rb +1 -0
  13. data/lib/mihari/{constraints.rb → constants.rb} +0 -0
  14. data/lib/mihari/database.rb +42 -3
  15. data/lib/mihari/mixins/rule.rb +5 -1
  16. data/lib/mihari/models/alert.rb +28 -10
  17. data/lib/mihari/models/artifact.rb +55 -0
  18. data/lib/mihari/models/autonomous_system.rb +9 -0
  19. data/lib/mihari/models/dns.rb +53 -0
  20. data/lib/mihari/models/geolocation.rb +9 -0
  21. data/lib/mihari/models/reverse_dns.rb +24 -0
  22. data/lib/mihari/models/whois.rb +119 -0
  23. data/lib/mihari/schemas/configuration.rb +1 -0
  24. data/lib/mihari/schemas/rule.rb +5 -15
  25. data/lib/mihari/serializers/alert.rb +6 -4
  26. data/lib/mihari/serializers/artifact.rb +11 -2
  27. data/lib/mihari/serializers/autonomous_system.rb +9 -0
  28. data/lib/mihari/serializers/dns.rb +11 -0
  29. data/lib/mihari/serializers/geolocation.rb +11 -0
  30. data/lib/mihari/serializers/reverse_dns.rb +11 -0
  31. data/lib/mihari/serializers/tag.rb +4 -2
  32. data/lib/mihari/serializers/whois.rb +11 -0
  33. data/lib/mihari/structs/censys.rb +92 -0
  34. data/lib/mihari/structs/onyphe.rb +47 -0
  35. data/lib/mihari/structs/shodan.rb +53 -0
  36. data/lib/mihari/templates/rule.yml.erb +3 -0
  37. data/lib/mihari/types.rb +21 -0
  38. data/lib/mihari/version.rb +1 -1
  39. data/lib/mihari/web/app.rb +2 -0
  40. data/lib/mihari/web/controllers/alerts_controller.rb +3 -4
  41. data/lib/mihari/web/controllers/artifacts_controller.rb +46 -2
  42. data/lib/mihari/web/controllers/ip_address_controller.rb +36 -0
  43. data/lib/mihari/web/controllers/sources_controller.rb +2 -2
  44. data/lib/mihari/web/controllers/tags_controller.rb +3 -1
  45. data/lib/mihari/web/public/index.html +1 -1
  46. data/lib/mihari/web/public/redoc-static.html +12 -10
  47. data/lib/mihari/web/public/static/fonts/fa-brands-400.1a575a41.woff +0 -0
  48. data/lib/mihari/web/public/static/fonts/fa-brands-400.513aa607.ttf +0 -0
  49. data/lib/mihari/web/public/static/fonts/fa-brands-400.592643a8.eot +0 -0
  50. data/lib/mihari/web/public/static/fonts/fa-brands-400.ed311c7a.woff2 +0 -0
  51. data/lib/mihari/web/public/static/fonts/fa-regular-400.766913e6.ttf +0 -0
  52. data/lib/mihari/web/public/static/fonts/fa-regular-400.b0e2db3b.eot +0 -0
  53. data/lib/mihari/web/public/static/fonts/fa-regular-400.b91d376b.woff2 +0 -0
  54. data/lib/mihari/web/public/static/fonts/fa-regular-400.d1d7e3b4.woff +0 -0
  55. data/lib/mihari/web/public/static/fonts/fa-solid-900.0c6bfc66.eot +0 -0
  56. data/lib/mihari/web/public/static/fonts/fa-solid-900.b9625119.ttf +0 -0
  57. data/lib/mihari/web/public/static/fonts/fa-solid-900.d745348d.woff +0 -0
  58. data/lib/mihari/web/public/static/fonts/fa-solid-900.d824df7e.woff2 +0 -0
  59. data/lib/mihari/web/public/static/img/fa-brands-400.1d5619cd.svg +3717 -0
  60. data/lib/mihari/web/public/static/img/fa-regular-400.c5d109be.svg +801 -0
  61. data/lib/mihari/web/public/static/img/fa-solid-900.37bc7099.svg +5034 -0
  62. data/lib/mihari/web/public/static/js/app.8e3e5150.js +36 -0
  63. data/lib/mihari/web/public/static/js/app.8e3e5150.js.map +1 -0
  64. data/lib/mihari/web/public/static/js/app.b5914c39.js +36 -0
  65. data/lib/mihari/web/public/static/js/app.b5914c39.js.map +1 -0
  66. data/lib/mihari.rb +25 -4
  67. data/mihari.gemspec +9 -2
  68. metadata +140 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 56538b2cc9269fa7e92a2c8cd98746caae7a480b7961fb83482a2189f5318992
4
- data.tar.gz: 9c4561c9e820a42b9b4d65b1e0ad02901034cc7eb124d7c5409418cab43bc5bd
3
+ metadata.gz: c9c1cbdf0570c25e2d89d7f6fd402b64991dfaebc75cf3cf5422a56504287ae9
4
+ data.tar.gz: d5b7a0db7b49f245e3c949135011fb674e28a9d3c251e165f827e8f3d90673b1
5
5
  SHA512:
6
- metadata.gz: 0b43cf2ee5e73607593e2c140dbc4ec70dcc8ad1eb436914a016f852f8dabed7f1aafd2f048565356f3fbed865540488e0766bc7f3a86bd6845c722419472abe
7
- data.tar.gz: 485bbcd01bcd8016b3a50b5e61fcac4cef8f527812570c734eff8a0fe6a75fc3b8e79e50d2a147c4edc20eee0370609ec5c2021bd28172bedff484bd04707379
6
+ metadata.gz: e76a216dedbc1aec17748c37a1b874c2c825fed6f7716ef356a48ddf2861584da299c384737e588a48b67165874f495192bb42a4c20c2f29f4620f8b559d1a83
7
+ data.tar.gz: 2f3e380b252ba238594ccacd2df8e362a62b9685aafeff34d6506e7301184cf5b38c4244db9498842f4aee9df109d7c43a9ad99cecc3b63616d333a7f5093333
data/README.md CHANGED
@@ -53,6 +53,10 @@ Mihari supports the following services by default.
53
53
 
54
54
  - [Mihari Knowledge Base](https://www.notion.so/Mihari-Knowledge-Base-266994ff61204428ba6cfcebe40b0bd1)
55
55
 
56
+ ## Presentations
57
+
58
+ - [Adversary Infrastructure Tracking with Mihari](https://ninoseki.github.io/presentations/Adversary%20Infrastructure%20Tracking%20with%20Mihari.pdf)
59
+
56
60
  ## License
57
61
 
58
62
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/config.ru CHANGED
@@ -1,3 +1,4 @@
1
+ # bundle exec rerun -- rackup config.ru
1
2
  require "./lib/mihari"
2
3
 
3
4
  # set rack env as development
@@ -51,7 +51,7 @@ module Mihari
51
51
  # @return [nil]
52
52
  #
53
53
  def run
54
- set_unique_artifacts
54
+ set_enriched_artifacts
55
55
 
56
56
  Parallel.each(valid_emitters) do |emitter|
57
57
  run_emitter emitter
@@ -66,7 +66,7 @@ module Mihari
66
66
  # @return [nil]
67
67
  #
68
68
  def run_emitter(emitter)
69
- emitter.run(title: title, description: description, artifacts: unique_artifacts, source: source, tags: tags)
69
+ emitter.run(title: title, description: description, artifacts: enriched_artifacts, source: source, tags: tags)
70
70
  rescue StandardError => e
71
71
  puts "Emission by #{emitter.class} is failed: #{e}"
72
72
  end
@@ -88,7 +88,7 @@ module Mihari
88
88
  # No need to set data_type manually
89
89
  # It is set automatically in #initialize
90
90
  artifact.is_a?(Artifact) ? artifact : Artifact.new(data: artifact, source: source)
91
- end.select(&:valid?)
91
+ end.select(&:valid?).uniq(&:data)
92
92
  end
93
93
 
94
94
  private
@@ -105,12 +105,26 @@ module Mihari
105
105
  end
106
106
 
107
107
  #
108
- # Set unique artifacts
108
+ # Enriched artifacts
109
+ #
110
+ # @return [Array<Mihari::Artifact>]
111
+ #
112
+ def enriched_artifacts
113
+ @enriched_artifacts ||= unique_artifacts.map do |artifact|
114
+ artifact.enrich_whois
115
+ artifact.enrich_dns
116
+ artifact.enrich_reverse_dns
117
+ artifact
118
+ end
119
+ end
120
+
121
+ #
122
+ # Set enriched artifacts
109
123
  #
110
124
  # @return [nil]
111
125
  #
112
- def set_unique_artifacts
113
- retry_on_error { unique_artifacts }
126
+ def set_enriched_artifacts
127
+ retry_on_error { enriched_artifacts }
114
128
  rescue ArgumentError => _e
115
129
  klass = self.class.to_s.split("::").last.to_s
116
130
  raise Error, "Please configure #{klass} API settings properly"
@@ -127,6 +141,20 @@ module Mihari
127
141
  emitter.valid? ? emitter : nil
128
142
  end
129
143
  end
144
+
145
+ #
146
+ # Normalize ASN value
147
+ #
148
+ # @param [String, Integer] asn
149
+ #
150
+ # @return [Integer]
151
+ #
152
+ def normalize_asn(asn)
153
+ return asn if asn.is_a?(Integer)
154
+ return asn.to_i unless asn.start_with?("AS")
155
+
156
+ asn.delete_prefix("AS").to_i
157
+ end
130
158
  end
131
159
  end
132
160
  end
@@ -17,31 +17,59 @@ module Mihari
17
17
  private
18
18
 
19
19
  def search
20
- ipv4s = []
20
+ artifacts = []
21
21
 
22
22
  cursor = nil
23
23
  loop do
24
24
  response = api.search(query, cursor: cursor)
25
- ipv4s << response_to_ipv4s(response)
25
+ response = Structs::Censys::Response.from_dynamic!(response)
26
26
 
27
- links = response.dig("result", "links")
28
- cursor = links["next"]
27
+ artifacts << response_to_artifacts(response)
28
+
29
+ cursor = response.result.links.next
29
30
  break if cursor == ""
30
31
  end
31
32
 
32
- ipv4s.flatten
33
+ artifacts.flatten.uniq(&:data)
33
34
  end
34
35
 
35
36
  #
36
37
  # Extract IPv4s from Censys search API response
37
38
  #
38
- # @param [Hash] response
39
+ # @param [Structs::Censys::Response] response
39
40
  #
40
41
  # @return [Array<String>]
41
42
  #
42
- def response_to_ipv4s(response)
43
- hits = response.dig("result", "hits") || []
44
- hits.map { |hit| hit["ip"] }
43
+ def response_to_artifacts(response)
44
+ response.result.hits.map { |hit| build_artifact(hit) }
45
+ end
46
+
47
+ #
48
+ # Build an artifact from a Shodan search API response
49
+ #
50
+ # @param [Structs::Censys::Hit] hit
51
+ #
52
+ # @return [Artifact]
53
+ #
54
+ def build_artifact(hit)
55
+ as = AutonomousSystem.new(asn: normalize_asn(hit.autonomous_system.asn))
56
+
57
+ # sometimes Censys overlooks country
58
+ # then set geolocation as nil
59
+ geolocation = nil
60
+ unless hit.location.country.nil?
61
+ geolocation = Geolocation.new(
62
+ country: hit.location.country,
63
+ country_code: hit.location.country_code
64
+ )
65
+ end
66
+
67
+ Artifact.new(
68
+ data: hit.ip,
69
+ source: source,
70
+ autonomous_system: as,
71
+ geolocation: geolocation
72
+ )
45
73
  end
46
74
 
47
75
  def configuration_keys
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "onyphe"
4
+ require "normalize_country"
4
5
 
5
6
  module Mihari
6
7
  module Analyzers
@@ -11,14 +12,13 @@ module Mihari
11
12
  option :tags, default: proc { [] }
12
13
 
13
14
  def artifacts
14
- results = search
15
- return [] unless results
15
+ responses = search
16
+ return [] unless responses
16
17
 
17
- flat_results = results.map do |result|
18
- result["results"]
19
- end.flatten.compact
20
-
21
- flat_results.filter_map { |result| result["ip"] }.uniq
18
+ results = responses.map(&:results).flatten
19
+ results.map do |result|
20
+ build_artifact result
21
+ end
22
22
  end
23
23
 
24
24
  private
@@ -34,7 +34,8 @@ module Mihari
34
34
  end
35
35
 
36
36
  def search_with_page(query, page: 1)
37
- api.simple.datascan(query, page: page)
37
+ res = api.simple.datascan(query, page: page)
38
+ Structs::Onyphe::Response.from_dynamic!(res)
38
39
  end
39
40
 
40
41
  def search
@@ -42,11 +43,35 @@ module Mihari
42
43
  (1..Float::INFINITY).each do |page|
43
44
  res = search_with_page(query, page: page)
44
45
  responses << res
45
- total = res["total"].to_i
46
+
47
+ total = res.total
46
48
  break if total <= page * PAGE_SIZE
47
49
  end
48
50
  responses
49
51
  end
52
+
53
+ #
54
+ # Build an artifact from an Onyphe search API result
55
+ #
56
+ # @param [Structs::Onyphe::Result] result
57
+ #
58
+ # @return [Artifact]
59
+ #
60
+ def build_artifact(result)
61
+ as = AutonomousSystem.new(asn: normalize_asn(result.asn))
62
+
63
+ geolocation = Geolocation.new(
64
+ country: NormalizeCountry(result.country_code, to: :short),
65
+ country_code: result.country_code
66
+ )
67
+
68
+ Artifact.new(
69
+ data: result.ip,
70
+ source: source,
71
+ autonomous_system: as,
72
+ geolocation: geolocation
73
+ )
74
+ end
50
75
  end
51
76
  end
52
77
  end
@@ -14,12 +14,11 @@ module Mihari
14
14
  results = search
15
15
  return [] unless results || results.empty?
16
16
 
17
+ results = results.map { |result| Structs::Shodan::Result.from_dynamic!(result) }
17
18
  results.map do |result|
18
- matches = result["matches"] || []
19
- matches.filter_map do |match|
20
- match["ip_str"]
21
- end
22
- end.flatten.compact.uniq
19
+ matches = result.matches || []
20
+ matches.map { |match| build_artifact match }
21
+ end.flatten.compact.uniq(&:data)
23
22
  end
24
23
 
25
24
  private
@@ -57,6 +56,28 @@ module Mihari
57
56
  end
58
57
  responses
59
58
  end
59
+
60
+ #
61
+ # Build an artifact from a Shodan search API response
62
+ #
63
+ # @param [Structs::Shodan::Match] match
64
+ #
65
+ # @return [Artifact]
66
+ #
67
+ def build_artifact(match)
68
+ as = AutonomousSystem.new(asn: normalize_asn(match.asn))
69
+ geolocation = Geolocation.new(
70
+ country: match.location.country_name,
71
+ country_code: match.location.country_code
72
+ )
73
+
74
+ Artifact.new(
75
+ data: match.ip_str,
76
+ source: source,
77
+ autonomous_system: as,
78
+ geolocation: geolocation
79
+ )
80
+ end
60
81
  end
61
82
  end
62
83
  end
@@ -22,6 +22,10 @@ require "mihari/commands/json"
22
22
  module Mihari
23
23
  module CLI
24
24
  class Analyzer < Base
25
+ class_option :ignore_old_artifacts, type: :boolean, default: false, desc: "Whether to ignore old artifacts from checking or not."
26
+ class_option :ignore_threshold, type: :numeric, default: 0, desc: "Number of days to define whether an artifact is old or not."
27
+ class_option :config, type: :string, desc: "Path to the config file"
28
+
25
29
  include Mihari::Commands::BinaryEdge
26
30
  include Mihari::Commands::Censys
27
31
  include Mihari::Commands::CIRCL
@@ -12,11 +12,6 @@ module Mihari
12
12
  include Mihari::Mixins::Hash
13
13
  include Mixins::Utils
14
14
 
15
- class_option :config, type: :string, desc: "Path to the config file"
16
-
17
- class_option :ignore_old_artifacts, type: :boolean, default: false, desc: "Whether to ignore old artifacts from checking or not. Only affects with analyze commands."
18
- class_option :ignore_threshold, type: :numeric, default: 0, desc: "Number of days to define whether an artifact is old or not. Only affects with analyze commands."
19
-
20
15
  class << self
21
16
  def exit_on_failure?
22
17
  true
@@ -5,10 +5,10 @@ require "colorize"
5
5
  module Mihari
6
6
  module Commands
7
7
  module Initialization
8
- def self.included(thor)
9
- include Mixins::Configuration
10
- include Mixins::Rule
8
+ include Mixins::Configuration
9
+ include Mixins::Rule
11
10
 
11
+ def self.included(thor)
12
12
  thor.class_eval do
13
13
  desc "config", "Create a config file"
14
14
  method_option :filename, type: :string, default: "mihari.yml"
@@ -37,7 +37,7 @@ module Mihari
37
37
 
38
38
  initialize_rule_yaml filename
39
39
 
40
- puts "The rule file is created as #{filename}.".colorize(:blue)
40
+ puts "The rule file is initialized as #{filename}.".colorize(:blue)
41
41
  end
42
42
  end
43
43
  end
@@ -8,17 +8,27 @@ module Mihari
8
8
  def self.included(thor)
9
9
  thor.class_eval do
10
10
  desc "search [RULE]", "Search by a rule"
11
+ method_option :config, type: :string, desc: "Path to the config file"
11
12
  def search_by_rule(rule)
12
13
  # convert str(YAML) to hash or str(path/YAML file) to hash
13
14
  rule = load_rule(rule)
14
15
 
15
16
  # validate rule schema
16
- validate_rule rule
17
+ rule = validate_rule(rule)
17
18
 
18
- analyzer = build_rule_analyzer(**rule)
19
+ analyzer = build_rule_analyzer(
20
+ title: rule[:title],
21
+ description: rule[:description],
22
+ queries: rule[:queries],
23
+ tags: rule[:tags],
24
+ allowed_data_types: rule[:allowed_data_types],
25
+ disallowed_data_values: rule[:disallowed_data_values],
26
+ source: rule[:source],
27
+ id: rule[:id]
28
+ )
19
29
 
20
- ignore_old_artifacts = options["ignore_old_artifacts"] || false
21
- ignore_threshold = options["ignore_threshold"] || 0
30
+ ignore_old_artifacts = rule[:ignore_old_artifacts]
31
+ ignore_threshold = rule[:ignore_threshold]
22
32
 
23
33
  with_error_handling do
24
34
  run_rule_analyzer analyzer, ignore_old_artifacts: ignore_old_artifacts, ignore_threshold: ignore_threshold
@@ -42,7 +52,7 @@ module Mihari
42
52
  #
43
53
  # @return [Mihari::Analyzers::Rule]
44
54
  #
45
- def build_rule_analyzer(title:, description:, queries:, tags: nil, allowed_data_types: nil, disallowed_data_values: nil, source: nil)
55
+ def build_rule_analyzer(title:, description:, queries:, tags: nil, allowed_data_types: nil, disallowed_data_values: nil, source: nil, id: nil)
46
56
  tags = [] if tags.nil?
47
57
  allowed_data_types = ALLOWED_DATA_TYPES if allowed_data_types.nil?
48
58
  disallowed_data_values = [] if disallowed_data_values.nil?
@@ -54,7 +64,8 @@ module Mihari
54
64
  queries: queries,
55
65
  allowed_data_types: allowed_data_types,
56
66
  disallowed_data_values: disallowed_data_values,
57
- source: source
67
+ source: source,
68
+ id: id
58
69
  )
59
70
  end
60
71
 
@@ -62,8 +73,6 @@ module Mihari
62
73
  # Run rule analyzer
63
74
  #
64
75
  # @param [Mihari::Analyzer::Rule] analyzer
65
- # @param [Boolean] ignore_old_artifacts
66
- # @param [Integer] ignore_threshold
67
76
  #
68
77
  # @return [nil]
69
78
  #
@@ -8,6 +8,7 @@ module Mihari
8
8
  desc "web", "Launch the web app"
9
9
  method_option :port, type: :numeric, default: 9292
10
10
  method_option :host, type: :string, default: "localhost"
11
+ method_option :config, type: :string, desc: "Path to the config file"
11
12
  def web
12
13
  port = options["port"].to_i || 9292
13
14
  host = options["host"] || "localhost"
File without changes
@@ -32,12 +32,48 @@ class InitialSchema < ActiveRecord::Migration[6.1]
32
32
  end
33
33
  end
34
34
 
35
- class V3Schema < ActiveRecord::Migration[6.1]
35
+ class AddeSourceToArtifactSchema < ActiveRecord::Migration[6.1]
36
36
  def change
37
37
  add_column :artifacts, :source, :string, if_not_exists: true
38
38
  end
39
39
  end
40
40
 
41
+ class EnrichmentsSchema < ActiveRecord::Migration[6.1]
42
+ def change
43
+ create_table :autonomous_systems, if_not_exists: true do |t|
44
+ t.integer :asn, null: false
45
+ t.belongs_to :artifact, foreign_key: true
46
+ end
47
+
48
+ create_table :geolocations, if_not_exists: true do |t|
49
+ t.string :country, null: false
50
+ t.string :country_code, null: false
51
+ t.belongs_to :artifact, foreign_key: true
52
+ end
53
+
54
+ create_table :whois_records, if_not_exists: true do |t|
55
+ t.string :domain, null: false
56
+ t.date :created_on
57
+ t.date :updated_on
58
+ t.date :expires_on
59
+ t.json :registrar
60
+ t.json :contacts
61
+ t.belongs_to :artifact, foreign_key: true
62
+ end
63
+
64
+ create_table :dns_records, if_not_exists: true do |t|
65
+ t.string :resource, null: false
66
+ t.string :value, null: false
67
+ t.belongs_to :artifact, foreign_key: true
68
+ end
69
+
70
+ create_table :reverse_dns_names, if_not_exists: true do |t|
71
+ t.string :name, null: false
72
+ t.belongs_to :artifact, foreign_key: true
73
+ end
74
+ end
75
+ end
76
+
41
77
  def adapter
42
78
  return "postgresql" if Mihari.config.database.start_with?("postgresql://", "postgres://")
43
79
  return "mysql2" if Mihari.config.database.start_with?("mysql2://")
@@ -59,10 +95,12 @@ module Mihari
59
95
  )
60
96
  end
61
97
 
98
+ # ActiveRecord::Base.logger = Logger.new STDOUT
62
99
  ActiveRecord::Migration.verbose = false
63
100
 
64
101
  InitialSchema.migrate(:up)
65
- V3Schema.migrate(:up)
102
+ AddeSourceToArtifactSchema.migrate(:up)
103
+ EnrichmentsSchema.migrate(:up)
66
104
  rescue StandardError
67
105
  # Do nothing
68
106
  end
@@ -76,7 +114,8 @@ module Mihari
76
114
  return unless ActiveRecord::Base.connected?
77
115
 
78
116
  InitialSchema.migrate(:down)
79
- V3Schema.migrate(:down)
117
+ AddeSourceToArtifactSchema.migrate(:down)
118
+ EnrichmentsSchema.migrate(:down)
80
119
  end
81
120
  end
82
121
  end