mihari 3.5.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.
- 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
|