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.
- checksums.yaml +4 -4
- data/README.md +4 -0
- 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/cli/analyzer.rb +4 -0
- data/lib/mihari/cli/base.rb +0 -5
- data/lib/mihari/commands/init.rb +4 -4
- data/lib/mihari/commands/search.rb +17 -8
- data/lib/mihari/commands/web.rb +1 -0
- data/lib/mihari/{constraints.rb → constants.rb} +0 -0
- data/lib/mihari/database.rb +42 -3
- data/lib/mihari/mixins/rule.rb +5 -1
- data/lib/mihari/models/alert.rb +28 -10
- 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/configuration.rb +1 -0
- data/lib/mihari/schemas/rule.rb +5 -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/templates/rule.yml.erb +3 -0
- data/lib/mihari/types.rb +21 -0
- data/lib/mihari/version.rb +1 -1
- data/lib/mihari/web/app.rb +2 -0
- data/lib/mihari/web/controllers/alerts_controller.rb +3 -4
- data/lib/mihari/web/controllers/artifacts_controller.rb +46 -2
- data/lib/mihari/web/controllers/ip_address_controller.rb +36 -0
- data/lib/mihari/web/controllers/sources_controller.rb +2 -2
- data/lib/mihari/web/controllers/tags_controller.rb +3 -1
- data/lib/mihari/web/public/index.html +1 -1
- data/lib/mihari/web/public/redoc-static.html +12 -10
- data/lib/mihari/web/public/static/fonts/fa-brands-400.1a575a41.woff +0 -0
- data/lib/mihari/web/public/static/fonts/fa-brands-400.513aa607.ttf +0 -0
- data/lib/mihari/web/public/static/fonts/fa-brands-400.592643a8.eot +0 -0
- data/lib/mihari/web/public/static/fonts/fa-brands-400.ed311c7a.woff2 +0 -0
- data/lib/mihari/web/public/static/fonts/fa-regular-400.766913e6.ttf +0 -0
- data/lib/mihari/web/public/static/fonts/fa-regular-400.b0e2db3b.eot +0 -0
- data/lib/mihari/web/public/static/fonts/fa-regular-400.b91d376b.woff2 +0 -0
- data/lib/mihari/web/public/static/fonts/fa-regular-400.d1d7e3b4.woff +0 -0
- data/lib/mihari/web/public/static/fonts/fa-solid-900.0c6bfc66.eot +0 -0
- data/lib/mihari/web/public/static/fonts/fa-solid-900.b9625119.ttf +0 -0
- data/lib/mihari/web/public/static/fonts/fa-solid-900.d745348d.woff +0 -0
- data/lib/mihari/web/public/static/fonts/fa-solid-900.d824df7e.woff2 +0 -0
- data/lib/mihari/web/public/static/img/fa-brands-400.1d5619cd.svg +3717 -0
- data/lib/mihari/web/public/static/img/fa-regular-400.c5d109be.svg +801 -0
- data/lib/mihari/web/public/static/img/fa-solid-900.37bc7099.svg +5034 -0
- 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/web/public/static/js/app.b5914c39.js +36 -0
- data/lib/mihari/web/public/static/js/app.b5914c39.js.map +1 -0
- data/lib/mihari.rb +25 -4
- data/mihari.gemspec +9 -2
- metadata +140 -8
data/lib/mihari/mixins/rule.rb
CHANGED
@@ -20,10 +20,12 @@ module Mihari
|
|
20
20
|
end
|
21
21
|
|
22
22
|
#
|
23
|
-
# Validate rule schema
|
23
|
+
# Validate rule schema and return a normalized rule
|
24
24
|
#
|
25
25
|
# @param [Hash] rule
|
26
26
|
#
|
27
|
+
# @return [Hash]
|
28
|
+
#
|
27
29
|
def validate_rule(rule)
|
28
30
|
error_message = "Failed to parse the input as a rule!"
|
29
31
|
|
@@ -42,6 +44,8 @@ module Mihari
|
|
42
44
|
puts error_message.colorize(:red)
|
43
45
|
raise ArgumentError, "Invalid rule schema"
|
44
46
|
end
|
47
|
+
|
48
|
+
result.to_h
|
45
49
|
end
|
46
50
|
|
47
51
|
#
|
data/lib/mihari/models/alert.rb
CHANGED
@@ -18,8 +18,8 @@ module Mihari
|
|
18
18
|
# @param [String, nil] source
|
19
19
|
# @param [String, nil] tag_name
|
20
20
|
# @param [String, nil] title
|
21
|
-
# @param [
|
22
|
-
# @param [
|
21
|
+
# @param [DateTime, nil] from_at
|
22
|
+
# @param [DateTime, nil] to_at
|
23
23
|
# @param [Integer, nil] limit
|
24
24
|
# @param [Integer, nil] page
|
25
25
|
#
|
@@ -34,12 +34,22 @@ module Mihari
|
|
34
34
|
|
35
35
|
offset = (page - 1) * limit
|
36
36
|
|
37
|
-
relation = build_relation(
|
37
|
+
relation = build_relation(
|
38
|
+
artifact_data: artifact_data,
|
39
|
+
title: title,
|
40
|
+
description: description,
|
41
|
+
source: source,
|
42
|
+
tag_name: tag_name,
|
43
|
+
from_at: from_at,
|
44
|
+
to_at: to_at
|
45
|
+
)
|
38
46
|
|
39
|
-
|
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)
|
40
50
|
|
41
51
|
alerts.map do |alert|
|
42
|
-
json = AlertSerializer.new(alert).as_json
|
52
|
+
json = Serializers::AlertSerializer.new(alert).as_json
|
43
53
|
json[:artifacts] = json[:artifacts] || []
|
44
54
|
json[:tags] = json[:tags] || []
|
45
55
|
json
|
@@ -54,13 +64,21 @@ module Mihari
|
|
54
64
|
# @param [String, nil] source
|
55
65
|
# @param [String, nil] tag_name
|
56
66
|
# @param [String, nil] title
|
57
|
-
# @param [
|
58
|
-
# @param [
|
67
|
+
# @param [DateTime, nil] from_at
|
68
|
+
# @param [DateTime, nil] to_at
|
59
69
|
#
|
60
70
|
# @return [Integer]
|
61
71
|
#
|
62
72
|
def count(artifact_data: nil, description: nil, source: nil, tag_name: nil, title: nil, from_at: nil, to_at: nil)
|
63
|
-
relation = build_relation(
|
73
|
+
relation = build_relation(
|
74
|
+
artifact_data: artifact_data,
|
75
|
+
title: title,
|
76
|
+
description: description,
|
77
|
+
source: source,
|
78
|
+
tag_name: tag_name,
|
79
|
+
from_at: from_at,
|
80
|
+
to_at: to_at
|
81
|
+
)
|
64
82
|
relation.distinct("alerts.id").count
|
65
83
|
end
|
66
84
|
|
@@ -68,8 +86,8 @@ module Mihari
|
|
68
86
|
|
69
87
|
def build_relation(artifact_data: nil, title: nil, description: nil, source: nil, tag_name: nil, from_at: nil, to_at: nil)
|
70
88
|
relation = self
|
71
|
-
|
72
|
-
relation =
|
89
|
+
|
90
|
+
relation = relation.includes(:artifacts, :tags)
|
73
91
|
|
74
92
|
relation = relation.where(artifacts: { data: artifact_data }) if artifact_data
|
75
93
|
relation = relation.where(tags: { name: tag_name }) if tag_name
|
@@ -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
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record"
|
4
|
+
require "resolv"
|
5
|
+
|
6
|
+
module Mihari
|
7
|
+
class DnsRecord < ActiveRecord::Base
|
8
|
+
class << self
|
9
|
+
#
|
10
|
+
# Build DNS records
|
11
|
+
#
|
12
|
+
# @param [String] domain
|
13
|
+
#
|
14
|
+
# @return [Array<Mihari::DnsRecord>]
|
15
|
+
#
|
16
|
+
def build_by_domain(domain)
|
17
|
+
resource_types = [
|
18
|
+
Resolv::DNS::Resource::IN::A,
|
19
|
+
Resolv::DNS::Resource::IN::AAAA,
|
20
|
+
Resolv::DNS::Resource::IN::CNAME,
|
21
|
+
Resolv::DNS::Resource::IN::TXT,
|
22
|
+
Resolv::DNS::Resource::IN::NS
|
23
|
+
]
|
24
|
+
|
25
|
+
resource_types.map do |resource_type|
|
26
|
+
get_values domain, resource_type
|
27
|
+
rescue Resolv::ResolvError
|
28
|
+
nil
|
29
|
+
end.flatten.compact
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def get_values(domain, resource_type)
|
35
|
+
resources = Resolv::DNS.new.getresources(domain, resource_type)
|
36
|
+
resource_name = resource_type.to_s.split("::").last
|
37
|
+
|
38
|
+
resources.map do |resource|
|
39
|
+
# A, AAAA
|
40
|
+
if resource.respond_to?(:address)
|
41
|
+
new(resource: resource_name, value: resource.address.to_s)
|
42
|
+
# CNAME, NS
|
43
|
+
elsif resource.respond_to?(:name)
|
44
|
+
new(resource: resource_name, value: resource.name.to_s)
|
45
|
+
# TXT
|
46
|
+
elsif resource.respond_to?(:data)
|
47
|
+
new(resource: resource_name, value: resource.data.to_s)
|
48
|
+
end
|
49
|
+
end.compact
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record"
|
4
|
+
require "resolv"
|
5
|
+
|
6
|
+
module Mihari
|
7
|
+
class ReverseDnsName < ActiveRecord::Base
|
8
|
+
class << self
|
9
|
+
#
|
10
|
+
# Build reverse DNS names
|
11
|
+
#
|
12
|
+
# @param [String] ip
|
13
|
+
#
|
14
|
+
# @return [Array<Mihari::ReverseDnsName>]
|
15
|
+
#
|
16
|
+
def build_by_ip(ip)
|
17
|
+
names = Resolv.getnames(ip)
|
18
|
+
names.map { |name| new(name: name) }
|
19
|
+
rescue Resolv::ResolvError
|
20
|
+
[]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record"
|
4
|
+
require "whois-parser"
|
5
|
+
require "public_suffix"
|
6
|
+
|
7
|
+
module Mihari
|
8
|
+
class WhoisRecord < ActiveRecord::Base
|
9
|
+
has_one :artifact, dependent: :destroy
|
10
|
+
|
11
|
+
@memo = {}
|
12
|
+
|
13
|
+
class << self
|
14
|
+
#
|
15
|
+
# Build whois record
|
16
|
+
#
|
17
|
+
# @param [Stinrg] domain
|
18
|
+
#
|
19
|
+
# @return [WhoisRecord, nil]
|
20
|
+
#
|
21
|
+
def build_by_domain(domain)
|
22
|
+
domain = PublicSuffix.domain(domain)
|
23
|
+
|
24
|
+
# check memo
|
25
|
+
if @memo.key?(domain)
|
26
|
+
whois_record = @memo[domain]
|
27
|
+
# return clone of the record
|
28
|
+
return whois_record.dup
|
29
|
+
end
|
30
|
+
|
31
|
+
record = Whois.whois(domain)
|
32
|
+
parser = record.parser
|
33
|
+
|
34
|
+
return nil if parser.available?
|
35
|
+
|
36
|
+
whois_record = new(
|
37
|
+
domain: domain,
|
38
|
+
created_on: get_created_on(parser),
|
39
|
+
updated_on: get_updated_on(parser),
|
40
|
+
expires_on: get_expires_on(parser),
|
41
|
+
registrar: get_registrar(parser),
|
42
|
+
contacts: get_contacts(parser)
|
43
|
+
)
|
44
|
+
# set memo
|
45
|
+
@memo[domain] = whois_record
|
46
|
+
whois_record
|
47
|
+
rescue Whois::Error, Whois::ParserError, Timeout::Error
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
#
|
54
|
+
# Get created_on
|
55
|
+
#
|
56
|
+
# @param [::Whois::Parser:] parser
|
57
|
+
#
|
58
|
+
# @return [Date, nil]
|
59
|
+
#
|
60
|
+
def get_created_on(parser)
|
61
|
+
parser.created_on
|
62
|
+
rescue ::Whois::AttributeNotImplemented
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# Get updated_on
|
68
|
+
#
|
69
|
+
# @param [::Whois::Parser:] parser
|
70
|
+
#
|
71
|
+
# @return [Date, nil]
|
72
|
+
#
|
73
|
+
def get_updated_on(parser)
|
74
|
+
parser.updated_on
|
75
|
+
rescue ::Whois::AttributeNotImplemented
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
|
79
|
+
#
|
80
|
+
# Get expires_on
|
81
|
+
#
|
82
|
+
# @param [::Whois::Parser:] parser
|
83
|
+
#
|
84
|
+
# @return [Date, nil]
|
85
|
+
#
|
86
|
+
def get_expires_on(parser)
|
87
|
+
parser.expires_on
|
88
|
+
rescue ::Whois::AttributeNotImplemented
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# Get registrar
|
94
|
+
#
|
95
|
+
# @param [::Whois::Parser:] parser
|
96
|
+
#
|
97
|
+
# @return [Hash, nil]
|
98
|
+
#
|
99
|
+
def get_registrar(parser)
|
100
|
+
parser.registrar&.to_h
|
101
|
+
rescue ::Whois::AttributeNotImplemented
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
|
105
|
+
#
|
106
|
+
# Get contacts
|
107
|
+
#
|
108
|
+
# @param [::Whois::Parser:] parser
|
109
|
+
#
|
110
|
+
# @return [Array[Hash], nil]
|
111
|
+
#
|
112
|
+
def get_contacts(parser)
|
113
|
+
parser.contacts.map(&:to_h)
|
114
|
+
rescue ::Whois::AttributeNotImplemented
|
115
|
+
nil
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -13,6 +13,7 @@ module Mihari
|
|
13
13
|
optional(:censys_secret).value(:string)
|
14
14
|
optional(:circl_passive_password).value(:string)
|
15
15
|
optional(:circl_passive_username).value(:string)
|
16
|
+
optional(:ipinfo_api_key).value(:string)
|
16
17
|
optional(:misp_api_endpoint).value(:string)
|
17
18
|
optional(:misp_api_key).value(:string)
|
18
19
|
optional(:onyphe_api_key).value(:string)
|
data/lib/mihari/schemas/rule.rb
CHANGED
@@ -2,26 +2,13 @@
|
|
2
2
|
|
3
3
|
require "dry/schema"
|
4
4
|
require "dry/validation"
|
5
|
-
require "dry/types"
|
6
5
|
|
7
6
|
require "mihari/schemas/macros"
|
8
7
|
|
9
8
|
module Mihari
|
10
|
-
module Types
|
11
|
-
include Dry.Types()
|
12
|
-
end
|
13
|
-
|
14
|
-
DataTypes = Types::String.enum(*ALLOWED_DATA_TYPES)
|
15
|
-
|
16
|
-
AnalyzerTypes = Types::String.enum(
|
17
|
-
"binaryedge", "censys", "circl", "dnpedia", "dnstwister",
|
18
|
-
"onyphe", "otx", "passivetotal", "pulsedive", "securitytrails",
|
19
|
-
"shodan", "virustotal"
|
20
|
-
)
|
21
|
-
|
22
9
|
module Schemas
|
23
10
|
Analyzer = Dry::Schema.Params do
|
24
|
-
required(:analyzer).value(AnalyzerTypes)
|
11
|
+
required(:analyzer).value(Types::AnalyzerTypes)
|
25
12
|
required(:query).value(:string)
|
26
13
|
end
|
27
14
|
|
@@ -62,8 +49,11 @@ module Mihari
|
|
62
49
|
|
63
50
|
required(:queries).value(:array).each { Analyzer | Spyse | ZoomEye | Urlscan | Crtsh }
|
64
51
|
|
65
|
-
optional(:allowed_data_types).value(array[DataTypes]).default(ALLOWED_DATA_TYPES)
|
52
|
+
optional(:allowed_data_types).value(array[Types::DataTypes]).default(ALLOWED_DATA_TYPES)
|
66
53
|
optional(:disallowed_data_values).value(array[:string]).default([])
|
54
|
+
|
55
|
+
optional(:ignore_old_artifacts).value(:bool).default(false)
|
56
|
+
optional(:ignore_threshold).value(:integer).default(0)
|
67
57
|
end
|
68
58
|
|
69
59
|
class RuleContract < Dry::Validation::Contract
|
@@ -3,10 +3,12 @@
|
|
3
3
|
require "active_model_serializers"
|
4
4
|
|
5
5
|
module Mihari
|
6
|
-
|
7
|
-
|
6
|
+
module Serializers
|
7
|
+
class AlertSerializer < ActiveModel::Serializer
|
8
|
+
attributes :id, :title, :description, :source, :created_at
|
8
9
|
|
9
|
-
|
10
|
-
|
10
|
+
has_many :artifacts, serializer: ArtifactSerializer
|
11
|
+
has_many :tags, through: :taggings, serializer: TagSerializer
|
12
|
+
end
|
11
13
|
end
|
12
14
|
end
|