mihari 3.5.0 → 3.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/config.ru +1 -0
  3. data/lib/mihari/analyzers/base.rb +34 -6
  4. data/lib/mihari/analyzers/censys.rb +37 -9
  5. data/lib/mihari/analyzers/onyphe.rb +34 -9
  6. data/lib/mihari/analyzers/shodan.rb +26 -5
  7. data/lib/mihari/{constraints.rb → constants.rb} +0 -0
  8. data/lib/mihari/database.rb +42 -3
  9. data/lib/mihari/models/alert.rb +8 -4
  10. data/lib/mihari/models/artifact.rb +55 -0
  11. data/lib/mihari/models/autonomous_system.rb +9 -0
  12. data/lib/mihari/models/dns.rb +53 -0
  13. data/lib/mihari/models/geolocation.rb +9 -0
  14. data/lib/mihari/models/reverse_dns.rb +24 -0
  15. data/lib/mihari/models/whois.rb +119 -0
  16. data/lib/mihari/schemas/rule.rb +2 -15
  17. data/lib/mihari/serializers/alert.rb +6 -4
  18. data/lib/mihari/serializers/artifact.rb +11 -2
  19. data/lib/mihari/serializers/autonomous_system.rb +9 -0
  20. data/lib/mihari/serializers/dns.rb +11 -0
  21. data/lib/mihari/serializers/geolocation.rb +11 -0
  22. data/lib/mihari/serializers/reverse_dns.rb +11 -0
  23. data/lib/mihari/serializers/tag.rb +4 -2
  24. data/lib/mihari/serializers/whois.rb +11 -0
  25. data/lib/mihari/structs/censys.rb +92 -0
  26. data/lib/mihari/structs/onyphe.rb +47 -0
  27. data/lib/mihari/structs/shodan.rb +53 -0
  28. data/lib/mihari/types.rb +21 -0
  29. data/lib/mihari/version.rb +1 -1
  30. data/lib/mihari/web/controllers/artifacts_controller.rb +26 -7
  31. data/lib/mihari/web/controllers/sources_controller.rb +2 -2
  32. data/lib/mihari/web/public/index.html +1 -1
  33. data/lib/mihari/web/public/redoc-static.html +2 -2
  34. data/lib/mihari/web/public/static/js/app.8e3e5150.js +36 -0
  35. data/lib/mihari/web/public/static/js/app.8e3e5150.js.map +1 -0
  36. data/lib/mihari.rb +24 -4
  37. data/mihari.gemspec +7 -1
  38. metadata +106 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a2fe0203f89908abbc21df8d595b7167a561c826b933166e531298b83f67e085
4
- data.tar.gz: 03a455ebd71f5d3c228041351b6ee8a6a2d89b74a7fc2f0d7609293bc82da7d6
3
+ metadata.gz: c9c1cbdf0570c25e2d89d7f6fd402b64991dfaebc75cf3cf5422a56504287ae9
4
+ data.tar.gz: d5b7a0db7b49f245e3c949135011fb674e28a9d3c251e165f827e8f3d90673b1
5
5
  SHA512:
6
- metadata.gz: aa4a28142eb109d46109d960c1de935ac4632136bc7696b405bfda3d691de2658764e7f796a8e9b1715973e0fc3cc9887997f3ab0dd054e4caa6baf6da40cb8e
7
- data.tar.gz: 057c5df7efd59ebf285b4a8d6a17a8857d8fa5df7b0c61f4ddbaf67d3688aaa4c34187af88acfe151a27a21b1d33b5e659347968ba6881e98e3b81114d113eb9
6
+ metadata.gz: e76a216dedbc1aec17748c37a1b874c2c825fed6f7716ef356a48ddf2861584da299c384737e588a48b67165874f495192bb42a4c20c2f29f4620f8b559d1a83
7
+ data.tar.gz: 2f3e380b252ba238594ccacd2df8e362a62b9685aafeff34d6506e7301184cf5b38c4244db9498842f4aee9df109d7c43a9ad99cecc3b63616d333a7f5093333
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
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
@@ -44,10 +44,12 @@ module Mihari
44
44
  to_at: to_at
45
45
  )
46
46
 
47
- alerts = relation.limit(limit).offset(offset).order(id: :desc)
47
+ # TODO: improve queires
48
+ alert_ids = relation.limit(limit).offset(offset).order(id: :desc).pluck(:id).uniq
49
+ alerts = includes(:artifacts, :tags).where(id: [alert_ids]).order(id: :desc)
48
50
 
49
51
  alerts.map do |alert|
50
- json = AlertSerializer.new(alert).as_json
52
+ json = Serializers::AlertSerializer.new(alert).as_json
51
53
  json[:artifacts] = json[:artifacts] || []
52
54
  json[:tags] = json[:tags] || []
53
55
  json
@@ -85,8 +87,10 @@ module Mihari
85
87
  def build_relation(artifact_data: nil, title: nil, description: nil, source: nil, tag_name: nil, from_at: nil, to_at: nil)
86
88
  relation = self
87
89
 
88
- relation = relation.joins(:artifacts).where(artifacts: { data: artifact_data }) if artifact_data
89
- relation = relation.joins(:tags).where(tags: { name: tag_name }) if tag_name
90
+ relation = relation.includes(:artifacts, :tags)
91
+
92
+ relation = relation.where(artifacts: { data: artifact_data }) if artifact_data
93
+ relation = relation.where(tags: { name: tag_name }) if tag_name
90
94
 
91
95
  relation = relation.where(source: source) if source
92
96
  relation = relation.where(title: title) if title
@@ -4,6 +4,7 @@ require "active_record"
4
4
  require "active_record/filter"
5
5
  require "active_support/core_ext/integer/time"
6
6
  require "active_support/core_ext/numeric/time"
7
+ require "uri"
7
8
 
8
9
  class ArtifactValidator < ActiveModel::Validator
9
10
  def validate(record)
@@ -15,6 +16,13 @@ end
15
16
 
16
17
  module Mihari
17
18
  class Artifact < ActiveRecord::Base
19
+ has_one :autonomous_system, dependent: :destroy
20
+ has_one :geolocation, dependent: :destroy
21
+ has_one :whois_record, dependent: :destroy
22
+
23
+ has_many :dns_records, dependent: :destroy
24
+ has_many :reverse_dns_names, dependent: :destroy
25
+
18
26
  include ActiveModel::Validations
19
27
 
20
28
  validates_with ArtifactValidator
@@ -44,5 +52,52 @@ module Mihari
44
52
  # within {ignore_threshold} days, do not ignore it
45
53
  artifact.created_at < days_before
46
54
  end
55
+
56
+ #
57
+ # Enrich(add) whois record
58
+ #
59
+ def enrich_whois
60
+ return unless can_enrich_whois?
61
+
62
+ self.whois_record = WhoisRecord.build_by_domain(normalize_as_domain(data))
63
+ end
64
+
65
+ #
66
+ # Enrich(add) DNS records
67
+ #
68
+ def enrich_dns
69
+ return unless can_enrich_dns?
70
+
71
+ self.dns_records = DnsRecord.build_by_domain(normalize_as_domain(data))
72
+ end
73
+
74
+ #
75
+ # Enrich(add) reverse DNS names
76
+ #
77
+ def enrich_reverse_dns
78
+ return unless can_enrich_revese_dns?
79
+
80
+ self.reverse_dns_names = ReverseDnsName.build_by_ip(data)
81
+ end
82
+
83
+ private
84
+
85
+ def normalize_as_domain(url_or_domain)
86
+ return url_or_domain if data_type == "domain"
87
+
88
+ URI.parse(url_or_domain).host
89
+ end
90
+
91
+ def can_enrich_whois?
92
+ %w[domain url].include? data_type
93
+ end
94
+
95
+ def can_enrich_dns?
96
+ %w[domain url].include? data_type
97
+ end
98
+
99
+ def can_enrich_revese_dns?
100
+ data_type == "ip"
101
+ end
47
102
  end
48
103
  end