mihari 3.5.0 → 3.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/config.ru +1 -0
- data/lib/mihari/analyzers/base.rb +34 -6
- data/lib/mihari/analyzers/censys.rb +37 -9
- data/lib/mihari/analyzers/onyphe.rb +34 -9
- data/lib/mihari/analyzers/shodan.rb +26 -5
- data/lib/mihari/{constraints.rb → constants.rb} +0 -0
- data/lib/mihari/database.rb +42 -3
- data/lib/mihari/models/alert.rb +8 -4
- data/lib/mihari/models/artifact.rb +55 -0
- data/lib/mihari/models/autonomous_system.rb +9 -0
- data/lib/mihari/models/dns.rb +53 -0
- data/lib/mihari/models/geolocation.rb +9 -0
- data/lib/mihari/models/reverse_dns.rb +24 -0
- data/lib/mihari/models/whois.rb +119 -0
- data/lib/mihari/schemas/rule.rb +2 -15
- data/lib/mihari/serializers/alert.rb +6 -4
- data/lib/mihari/serializers/artifact.rb +11 -2
- data/lib/mihari/serializers/autonomous_system.rb +9 -0
- data/lib/mihari/serializers/dns.rb +11 -0
- data/lib/mihari/serializers/geolocation.rb +11 -0
- data/lib/mihari/serializers/reverse_dns.rb +11 -0
- data/lib/mihari/serializers/tag.rb +4 -2
- data/lib/mihari/serializers/whois.rb +11 -0
- data/lib/mihari/structs/censys.rb +92 -0
- data/lib/mihari/structs/onyphe.rb +47 -0
- data/lib/mihari/structs/shodan.rb +53 -0
- data/lib/mihari/types.rb +21 -0
- data/lib/mihari/version.rb +1 -1
- data/lib/mihari/web/controllers/artifacts_controller.rb +26 -7
- data/lib/mihari/web/controllers/sources_controller.rb +2 -2
- data/lib/mihari/web/public/index.html +1 -1
- data/lib/mihari/web/public/redoc-static.html +2 -2
- data/lib/mihari/web/public/static/js/app.8e3e5150.js +36 -0
- data/lib/mihari/web/public/static/js/app.8e3e5150.js.map +1 -0
- data/lib/mihari.rb +24 -4
- data/mihari.gemspec +7 -1
- metadata +106 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c9c1cbdf0570c25e2d89d7f6fd402b64991dfaebc75cf3cf5422a56504287ae9
|
4
|
+
data.tar.gz: d5b7a0db7b49f245e3c949135011fb674e28a9d3c251e165f827e8f3d90673b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e76a216dedbc1aec17748c37a1b874c2c825fed6f7716ef356a48ddf2861584da299c384737e588a48b67165874f495192bb42a4c20c2f29f4620f8b559d1a83
|
7
|
+
data.tar.gz: 2f3e380b252ba238594ccacd2df8e362a62b9685aafeff34d6506e7301184cf5b38c4244db9498842f4aee9df109d7c43a9ad99cecc3b63616d333a7f5093333
|
data/config.ru
CHANGED
@@ -51,7 +51,7 @@ module Mihari
|
|
51
51
|
# @return [nil]
|
52
52
|
#
|
53
53
|
def run
|
54
|
-
|
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:
|
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
|
-
#
|
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
|
113
|
-
retry_on_error {
|
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
|
-
|
20
|
+
artifacts = []
|
21
21
|
|
22
22
|
cursor = nil
|
23
23
|
loop do
|
24
24
|
response = api.search(query, cursor: cursor)
|
25
|
-
|
25
|
+
response = Structs::Censys::Response.from_dynamic!(response)
|
26
26
|
|
27
|
-
|
28
|
-
|
27
|
+
artifacts << response_to_artifacts(response)
|
28
|
+
|
29
|
+
cursor = response.result.links.next
|
29
30
|
break if cursor == ""
|
30
31
|
end
|
31
32
|
|
32
|
-
|
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 [
|
39
|
+
# @param [Structs::Censys::Response] response
|
39
40
|
#
|
40
41
|
# @return [Array<String>]
|
41
42
|
#
|
42
|
-
def
|
43
|
-
hits
|
44
|
-
|
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
|
-
|
15
|
-
return [] unless
|
15
|
+
responses = search
|
16
|
+
return [] unless responses
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
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
|
19
|
-
matches.
|
20
|
-
|
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
|
data/lib/mihari/database.rb
CHANGED
@@ -32,12 +32,48 @@ class InitialSchema < ActiveRecord::Migration[6.1]
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
-
class
|
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
|
-
|
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
|
-
|
117
|
+
AddeSourceToArtifactSchema.migrate(:down)
|
118
|
+
EnrichmentsSchema.migrate(:down)
|
80
119
|
end
|
81
120
|
end
|
82
121
|
end
|
data/lib/mihari/models/alert.rb
CHANGED
@@ -44,10 +44,12 @@ module Mihari
|
|
44
44
|
to_at: to_at
|
45
45
|
)
|
46
46
|
|
47
|
-
|
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.
|
89
|
-
|
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
|