mihari 3.3.0 → 3.6.0

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