mihari 5.6.1 → 5.6.2
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/frontend/package-lock.json +173 -176
- data/frontend/package.json +9 -9
- data/lib/mihari/{base.rb → actor.rb} +16 -2
- data/lib/mihari/analyzers/base.rb +5 -10
- data/lib/mihari/analyzers/censys.rb +1 -1
- data/lib/mihari/analyzers/hunterhow.rb +1 -1
- data/lib/mihari/analyzers/passivetotal.rb +1 -1
- data/lib/mihari/analyzers/pulsedive.rb +1 -1
- data/lib/mihari/analyzers/securitytrails.rb +1 -1
- data/lib/mihari/analyzers/urlscan.rb +1 -1
- data/lib/mihari/analyzers/virustotal.rb +5 -5
- data/lib/mihari/analyzers/zoomeye.rb +3 -3
- data/lib/mihari/clients/crtsh.rb +2 -2
- data/lib/mihari/clients/passivetotal.rb +4 -4
- data/lib/mihari/clients/securitytrails.rb +3 -3
- data/lib/mihari/commands/rule.rb +2 -11
- data/lib/mihari/commands/search.rb +1 -1
- data/lib/mihari/emitters/base.rb +13 -24
- data/lib/mihari/emitters/database.rb +7 -9
- data/lib/mihari/emitters/misp.rb +14 -38
- data/lib/mihari/emitters/slack.rb +14 -11
- data/lib/mihari/emitters/the_hive.rb +16 -44
- data/lib/mihari/emitters/webhook.rb +31 -21
- data/lib/mihari/enrichers/base.rb +1 -6
- data/lib/mihari/enrichers/whois.rb +1 -1
- data/lib/mihari/models/alert.rb +75 -73
- data/lib/mihari/models/artifact.rb +182 -180
- data/lib/mihari/models/autonomous_system.rb +22 -20
- data/lib/mihari/models/cpe.rb +21 -19
- data/lib/mihari/models/dns.rb +24 -22
- data/lib/mihari/models/geolocation.rb +22 -20
- data/lib/mihari/models/port.rb +21 -19
- data/lib/mihari/models/reverse_dns.rb +21 -19
- data/lib/mihari/models/rule.rb +67 -65
- data/lib/mihari/models/tag.rb +5 -3
- data/lib/mihari/models/tagging.rb +5 -3
- data/lib/mihari/models/whois.rb +18 -16
- data/lib/mihari/rule.rb +352 -0
- data/lib/mihari/schemas/analyzer.rb +94 -87
- data/lib/mihari/schemas/emitter.rb +9 -5
- data/lib/mihari/schemas/enricher.rb +8 -4
- data/lib/mihari/schemas/mixins.rb +15 -0
- data/lib/mihari/schemas/rule.rb +3 -10
- data/lib/mihari/services/alert_builder.rb +1 -1
- data/lib/mihari/services/alert_proxy.rb +10 -6
- data/lib/mihari/services/alert_runner.rb +4 -4
- data/lib/mihari/services/rule_builder.rb +3 -3
- data/lib/mihari/services/rule_runner.rb +5 -5
- data/lib/mihari/structs/binaryedge.rb +1 -1
- data/lib/mihari/structs/censys.rb +6 -6
- data/lib/mihari/structs/config.rb +1 -1
- data/lib/mihari/structs/greynoise.rb +5 -5
- data/lib/mihari/structs/hunterhow.rb +3 -3
- data/lib/mihari/structs/onyphe.rb +5 -5
- data/lib/mihari/structs/shodan.rb +6 -6
- data/lib/mihari/structs/urlscan.rb +3 -3
- data/lib/mihari/structs/virustotal_intelligence.rb +3 -3
- data/lib/mihari/version.rb +1 -1
- data/lib/mihari/web/endpoints/alerts.rb +4 -4
- data/lib/mihari/web/endpoints/artifacts.rb +6 -6
- data/lib/mihari/web/endpoints/rules.rb +10 -17
- data/lib/mihari/web/endpoints/tags.rb +2 -2
- data/lib/mihari/web/public/assets/{index-9cc489e6.js → index-28d4c79d.js} +48 -48
- data/lib/mihari/web/public/index.html +1 -1
- data/lib/mihari.rb +6 -8
- data/mihari.gemspec +1 -2
- data/requirements.txt +1 -1
- metadata +8 -22
- data/lib/mihari/analyzers/rule.rb +0 -232
- data/lib/mihari/services/rule_proxy.rb +0 -182
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Mihari
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
module Models
|
|
5
|
+
class ReverseDnsName < ActiveRecord::Base
|
|
6
|
+
belongs_to :artifact
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
class << self
|
|
9
|
+
include Dry::Monads[:result]
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
11
|
+
#
|
|
12
|
+
# Build reverse DNS names
|
|
13
|
+
#
|
|
14
|
+
# @param [String] ip
|
|
15
|
+
# @param [Mihari::Enrichers::Shodan] enricher
|
|
16
|
+
#
|
|
17
|
+
# @return [Array<Mihari::Models::ReverseDnsName>]
|
|
18
|
+
#
|
|
19
|
+
def build_by_ip(ip, enricher: Enrichers::Shodan.new)
|
|
20
|
+
result = enricher.query_result(ip).bind do |res|
|
|
21
|
+
if res.nil?
|
|
22
|
+
Success []
|
|
23
|
+
else
|
|
24
|
+
Success(res.hostnames.map { |name| new(name: name) })
|
|
25
|
+
end
|
|
24
26
|
end
|
|
27
|
+
result.value_or []
|
|
25
28
|
end
|
|
26
|
-
result.value_or []
|
|
27
29
|
end
|
|
28
30
|
end
|
|
29
31
|
end
|
data/lib/mihari/models/rule.rb
CHANGED
|
@@ -3,77 +3,79 @@
|
|
|
3
3
|
require "yaml"
|
|
4
4
|
|
|
5
5
|
module Mihari
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
module Models
|
|
7
|
+
class Rule < ActiveRecord::Base
|
|
8
|
+
has_many :alerts, dependent: :destroy
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def yaml
|
|
14
|
-
data.to_yaml
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def tags
|
|
18
|
-
(data["tags"] || []).map { |tag| { name: tag } }
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
class << self
|
|
22
|
-
#
|
|
23
|
-
# Search rules
|
|
24
|
-
#
|
|
25
|
-
# @param [Structs::Filters::Rule::SearchFilterWithPagination] filter
|
|
26
|
-
#
|
|
27
|
-
# @return [Array<Rule>]
|
|
28
|
-
#
|
|
29
|
-
def search(filter)
|
|
30
|
-
limit = filter.limit.to_i
|
|
31
|
-
raise ArgumentError, "limit should be bigger than zero" unless limit.positive?
|
|
32
|
-
|
|
33
|
-
page = filter.page.to_i
|
|
34
|
-
raise ArgumentError, "page should be bigger than zero" unless page.positive?
|
|
35
|
-
|
|
36
|
-
offset = (page - 1) * limit
|
|
37
|
-
|
|
38
|
-
relation = build_relation(filter.without_pagination)
|
|
39
|
-
|
|
40
|
-
# TODO: improve queires
|
|
41
|
-
rule_ids = relation.limit(limit).offset(offset).order(created_at: :desc).pluck(:id).uniq
|
|
42
|
-
where(id: [rule_ids]).order(created_at: :desc)
|
|
10
|
+
def symbolized_data
|
|
11
|
+
@symbolized_data ||= data.deep_symbolize_keys
|
|
43
12
|
end
|
|
44
13
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
#
|
|
48
|
-
# @param [Structs::Filters::Rule::SearchFilterWithPagination] filter
|
|
49
|
-
#
|
|
50
|
-
# @return [Integer]
|
|
51
|
-
#
|
|
52
|
-
def count(filter)
|
|
53
|
-
relation = build_relation(filter)
|
|
54
|
-
relation.distinct("rules.id").count
|
|
14
|
+
def yaml
|
|
15
|
+
data.to_yaml
|
|
55
16
|
end
|
|
56
17
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
# @param [Structs::Filters::Rule::SearchFilter] filter
|
|
61
|
-
#
|
|
62
|
-
# @return [Mihari::Rule]
|
|
63
|
-
#
|
|
64
|
-
def build_relation(filter)
|
|
65
|
-
relation = self
|
|
66
|
-
relation = relation.includes(alerts: :tags)
|
|
67
|
-
|
|
68
|
-
relation = relation.where(alerts: { tags: { name: filter.tag_name } }) if filter.tag_name
|
|
69
|
-
|
|
70
|
-
relation = relation.where("rules.title LIKE ?", "%#{filter.title}%") if filter.title
|
|
71
|
-
relation = relation.where("rules.description LIKE ?", "%#{filter.description}%") if filter.description
|
|
72
|
-
|
|
73
|
-
relation = relation.where("rules.created_at >= ?", filter.from_at) if filter.from_at
|
|
74
|
-
relation = relation.where("rules.created_at <= ?", filter.to_at) if filter.to_at
|
|
18
|
+
def tags
|
|
19
|
+
(data["tags"] || []).map { |tag| { name: tag } }
|
|
20
|
+
end
|
|
75
21
|
|
|
76
|
-
|
|
22
|
+
class << self
|
|
23
|
+
#
|
|
24
|
+
# Search rules
|
|
25
|
+
#
|
|
26
|
+
# @param [Structs::Filters::Rule::SearchFilterWithPagination] filter
|
|
27
|
+
#
|
|
28
|
+
# @return [Array<Rule>]
|
|
29
|
+
#
|
|
30
|
+
def search(filter)
|
|
31
|
+
limit = filter.limit.to_i
|
|
32
|
+
raise ArgumentError, "limit should be bigger than zero" unless limit.positive?
|
|
33
|
+
|
|
34
|
+
page = filter.page.to_i
|
|
35
|
+
raise ArgumentError, "page should be bigger than zero" unless page.positive?
|
|
36
|
+
|
|
37
|
+
offset = (page - 1) * limit
|
|
38
|
+
|
|
39
|
+
relation = build_relation(filter.without_pagination)
|
|
40
|
+
|
|
41
|
+
# TODO: improve queires
|
|
42
|
+
rule_ids = relation.limit(limit).offset(offset).order(created_at: :desc).pluck(:id).uniq
|
|
43
|
+
where(id: [rule_ids]).order(created_at: :desc)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
#
|
|
47
|
+
# Count alerts
|
|
48
|
+
#
|
|
49
|
+
# @param [Structs::Filters::Rule::SearchFilterWithPagination] filter
|
|
50
|
+
#
|
|
51
|
+
# @return [Integer]
|
|
52
|
+
#
|
|
53
|
+
def count(filter)
|
|
54
|
+
relation = build_relation(filter)
|
|
55
|
+
relation.distinct("rules.id").count
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
#
|
|
61
|
+
# @param [Structs::Filters::Rule::SearchFilter] filter
|
|
62
|
+
#
|
|
63
|
+
# @return [Mihari::Models::Rule]
|
|
64
|
+
#
|
|
65
|
+
def build_relation(filter)
|
|
66
|
+
relation = self
|
|
67
|
+
relation = relation.includes(alerts: :tags)
|
|
68
|
+
|
|
69
|
+
relation = relation.where(alerts: { tags: { name: filter.tag_name } }) if filter.tag_name
|
|
70
|
+
|
|
71
|
+
relation = relation.where("rules.title LIKE ?", "%#{filter.title}%") if filter.title
|
|
72
|
+
relation = relation.where("rules.description LIKE ?", "%#{filter.description}%") if filter.description
|
|
73
|
+
|
|
74
|
+
relation = relation.where("rules.created_at >= ?", filter.from_at) if filter.from_at
|
|
75
|
+
relation = relation.where("rules.created_at <= ?", filter.to_at) if filter.to_at
|
|
76
|
+
|
|
77
|
+
relation
|
|
78
|
+
end
|
|
77
79
|
end
|
|
78
80
|
end
|
|
79
81
|
end
|
data/lib/mihari/models/tag.rb
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Mihari
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
module Models
|
|
5
|
+
class Tag < ActiveRecord::Base
|
|
6
|
+
has_many :taggings, dependent: :destroy
|
|
7
|
+
has_many :tags, through: :taggings
|
|
8
|
+
end
|
|
7
9
|
end
|
|
8
10
|
end
|
data/lib/mihari/models/whois.rb
CHANGED
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Mihari
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
module Models
|
|
5
|
+
class WhoisRecord < ActiveRecord::Base
|
|
6
|
+
belongs_to :artifact
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
@memo = {}
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
class << self
|
|
11
|
+
include Dry::Monads[:result]
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
#
|
|
14
|
+
# Build whois record
|
|
15
|
+
#
|
|
16
|
+
# @param [String] domain
|
|
17
|
+
# @param [Mihari::Enrichers::Whois] enricher
|
|
18
|
+
#
|
|
19
|
+
# @return [WhoisRecord, nil]
|
|
20
|
+
#
|
|
21
|
+
def build_by_domain(domain, enricher: Enrichers::Whois.new)
|
|
22
|
+
result = enricher.query_result(domain)
|
|
23
|
+
result.value_or nil
|
|
24
|
+
end
|
|
23
25
|
end
|
|
24
26
|
end
|
|
25
27
|
end
|
data/lib/mihari/rule.rb
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mihari
|
|
4
|
+
class Rule
|
|
5
|
+
include Mixins::FalsePositive
|
|
6
|
+
|
|
7
|
+
# @return [Hash]
|
|
8
|
+
attr_reader :data
|
|
9
|
+
|
|
10
|
+
# @return [Array, nil]
|
|
11
|
+
attr_reader :errors
|
|
12
|
+
|
|
13
|
+
# @return [Time]
|
|
14
|
+
attr_reader :base_time
|
|
15
|
+
|
|
16
|
+
#
|
|
17
|
+
# Initialize
|
|
18
|
+
#
|
|
19
|
+
# @param [Hash] data
|
|
20
|
+
#
|
|
21
|
+
def initialize(**data)
|
|
22
|
+
@data = data.deep_symbolize_keys
|
|
23
|
+
@errors = nil
|
|
24
|
+
@base_time = Time.now.utc
|
|
25
|
+
|
|
26
|
+
validate!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
#
|
|
30
|
+
# @return [Boolean]
|
|
31
|
+
#
|
|
32
|
+
def errors?
|
|
33
|
+
return false if @errors.nil?
|
|
34
|
+
|
|
35
|
+
!@errors.empty?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def [](key)
|
|
39
|
+
data key.to_sym
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
#
|
|
43
|
+
# @return [String]
|
|
44
|
+
#
|
|
45
|
+
def id
|
|
46
|
+
data[:id]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
#
|
|
50
|
+
# @return [String]
|
|
51
|
+
#
|
|
52
|
+
def title
|
|
53
|
+
data[:title]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
#
|
|
57
|
+
# @return [String]
|
|
58
|
+
#
|
|
59
|
+
def description
|
|
60
|
+
data[:description]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
#
|
|
64
|
+
# @return [String]
|
|
65
|
+
#
|
|
66
|
+
def yaml
|
|
67
|
+
data.deep_stringify_keys.to_yaml
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
#
|
|
71
|
+
# @return [Array<Hash>]
|
|
72
|
+
#
|
|
73
|
+
def queries
|
|
74
|
+
data[:queries]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
#
|
|
78
|
+
# @return [Array<String>]
|
|
79
|
+
#
|
|
80
|
+
def data_types
|
|
81
|
+
data[:data_types]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
#
|
|
85
|
+
# @return [Array<String>]
|
|
86
|
+
#
|
|
87
|
+
def tags
|
|
88
|
+
data[:tags]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
#
|
|
92
|
+
# @return [Array<String, RegExp>]
|
|
93
|
+
#
|
|
94
|
+
def falsepositives
|
|
95
|
+
@falsepositives ||= data[:falsepositives].map { |fp| normalize_falsepositive fp }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
#
|
|
99
|
+
# @return [Integer, nil]
|
|
100
|
+
#
|
|
101
|
+
def artifact_lifetime
|
|
102
|
+
data[:artifact_lifetime] || data[:artifact_ttl]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
#
|
|
106
|
+
# Returns a list of artifacts matched with queries/analyzers (with the rule ID)
|
|
107
|
+
#
|
|
108
|
+
# @return [Array<Mihari::Models::Artifact>]
|
|
109
|
+
#
|
|
110
|
+
def artifacts
|
|
111
|
+
analyzers.flat_map do |analyzer|
|
|
112
|
+
result = analyzer.result
|
|
113
|
+
|
|
114
|
+
raise result.failure if result.failure? && !analyzer.ignore_error?
|
|
115
|
+
|
|
116
|
+
artifacts = result.value!
|
|
117
|
+
artifacts.map do |artifact|
|
|
118
|
+
artifact.rule_id = id
|
|
119
|
+
artifact
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
#
|
|
125
|
+
# Normalize artifacts
|
|
126
|
+
# - Reject invalid artifacts (for just in case)
|
|
127
|
+
# - Select artifacts with allowed data types
|
|
128
|
+
# - Reject artifacts with false positive values
|
|
129
|
+
# - Set rule ID
|
|
130
|
+
#
|
|
131
|
+
# @return [Array<Mihari::Models::Artifact>]
|
|
132
|
+
#
|
|
133
|
+
def normalized_artifacts
|
|
134
|
+
valid_artifacts = artifacts.uniq(&:data).select(&:valid?)
|
|
135
|
+
date_type_allowed_artifacts = valid_artifacts.select { |artifact| data_types.include? artifact.data_type }
|
|
136
|
+
date_type_allowed_artifacts.reject { |artifact| falsepositive? artifact.data }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
#
|
|
140
|
+
# Uniquify artifacts (assure rule level uniqueness)
|
|
141
|
+
#
|
|
142
|
+
# @return [Array<Mihari::Models::Artifact>]
|
|
143
|
+
#
|
|
144
|
+
def unique_artifacts
|
|
145
|
+
normalized_artifacts.select do |artifact|
|
|
146
|
+
artifact.unique?(base_time: base_time, artifact_lifetime: artifact_lifetime)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
#
|
|
151
|
+
# Enriched artifacts
|
|
152
|
+
#
|
|
153
|
+
# @return [Array<Mihari::Models::Artifact>]
|
|
154
|
+
#
|
|
155
|
+
def enriched_artifacts
|
|
156
|
+
@enriched_artifacts ||= Parallel.map(unique_artifacts) do |artifact|
|
|
157
|
+
enrichers.each { |enricher| artifact.enrich_by_enricher enricher }
|
|
158
|
+
artifact
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
#
|
|
163
|
+
# Bulk emit
|
|
164
|
+
#
|
|
165
|
+
# @return [Array<Mihari::Models::Alert>]
|
|
166
|
+
#
|
|
167
|
+
def bulk_emit
|
|
168
|
+
return [] if enriched_artifacts.empty?
|
|
169
|
+
|
|
170
|
+
# NOTE: separate parallel execution and logging
|
|
171
|
+
# because the logger does not work along with Parallel
|
|
172
|
+
results = Parallel.map(emitters) { |emitter| emitter.emit_result(enriched_artifacts) }
|
|
173
|
+
results.zip(emitters).map do |result_and_emitter|
|
|
174
|
+
result, emitter = result_and_emitter
|
|
175
|
+
Mihari.logger.info "Emission by #{emitter.class} is failed: #{result.failure}" if result.failure?
|
|
176
|
+
Mihari.logger.info "Emission by #{emitter.class} is succeeded" if result.success?
|
|
177
|
+
result.value_or nil
|
|
178
|
+
end.compact
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
#
|
|
182
|
+
# Set artifacts & run emitters in parallel
|
|
183
|
+
#
|
|
184
|
+
# @return [Mihari::Models::Alert, nil]
|
|
185
|
+
#
|
|
186
|
+
def run
|
|
187
|
+
# Validate analyzers & emitters before using them
|
|
188
|
+
analyzers
|
|
189
|
+
emitters
|
|
190
|
+
|
|
191
|
+
alert_or_something = bulk_emit
|
|
192
|
+
# Return Mihari::Models::Alert created by the database emitter
|
|
193
|
+
alert_or_something.find { |res| res.is_a?(Mihari::Models::Alert) }
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
#
|
|
197
|
+
# @return [Mihari::Models::Rule]
|
|
198
|
+
#
|
|
199
|
+
def model
|
|
200
|
+
rule = Mihari::Models::Rule.find(id)
|
|
201
|
+
|
|
202
|
+
rule.title = title
|
|
203
|
+
rule.description = description
|
|
204
|
+
rule.data = data
|
|
205
|
+
|
|
206
|
+
rule
|
|
207
|
+
rescue ActiveRecord::RecordNotFound
|
|
208
|
+
Mihari::Models::Rule.new(
|
|
209
|
+
id: id,
|
|
210
|
+
title: title,
|
|
211
|
+
description: description,
|
|
212
|
+
data: data
|
|
213
|
+
)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
class << self
|
|
217
|
+
#
|
|
218
|
+
# Load rule from YAML string
|
|
219
|
+
#
|
|
220
|
+
# @param [String] yaml
|
|
221
|
+
#
|
|
222
|
+
# @return [Mihari::Services::Rule]
|
|
223
|
+
#
|
|
224
|
+
def from_yaml(yaml)
|
|
225
|
+
data = YAML.safe_load(ERB.new(yaml).result, permitted_classes: [Date, Symbol])
|
|
226
|
+
new(**data)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
#
|
|
230
|
+
# @param [Mihari::Models::Rule] model
|
|
231
|
+
#
|
|
232
|
+
# @return [Mihari::Services::Rule]
|
|
233
|
+
#
|
|
234
|
+
def from_model(model)
|
|
235
|
+
new(**model.data)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
private
|
|
240
|
+
|
|
241
|
+
#
|
|
242
|
+
# Check whether a value is a falsepositive value or not
|
|
243
|
+
#
|
|
244
|
+
# @return [Boolean]
|
|
245
|
+
#
|
|
246
|
+
def falsepositive?(value)
|
|
247
|
+
return true if falsepositives.include?(value)
|
|
248
|
+
|
|
249
|
+
regexps = falsepositives.select { |fp| fp.is_a?(Regexp) }
|
|
250
|
+
regexps.any? { |fp| fp.match?(value) }
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
#
|
|
254
|
+
# Get analyzer class
|
|
255
|
+
#
|
|
256
|
+
# @param [String] key
|
|
257
|
+
#
|
|
258
|
+
# @return [Class<Mihari::Analyzers::Base>] analyzer class
|
|
259
|
+
#
|
|
260
|
+
def get_analyzer_class(key)
|
|
261
|
+
raise ArgumentError, "#{key} is not supported" unless Mihari.analyzer_to_class.key?(key)
|
|
262
|
+
|
|
263
|
+
Mihari.analyzer_to_class[key]
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
#
|
|
267
|
+
# @return [Array<Mihari::Analyzers::Base>]
|
|
268
|
+
#
|
|
269
|
+
def analyzers
|
|
270
|
+
@analyzers ||= queries.map do |query_params|
|
|
271
|
+
analyzer_name = query_params[:analyzer]
|
|
272
|
+
klass = get_analyzer_class(analyzer_name)
|
|
273
|
+
klass.from_query(query_params)
|
|
274
|
+
end.map do |analyzer|
|
|
275
|
+
analyzer.validate_configuration!
|
|
276
|
+
analyzer
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
#
|
|
281
|
+
# Get emitter class
|
|
282
|
+
#
|
|
283
|
+
# @param [String] key
|
|
284
|
+
#
|
|
285
|
+
# @return [Class<Mihari::Emitters::Base>] emitter class
|
|
286
|
+
#
|
|
287
|
+
def get_emitter_class(key)
|
|
288
|
+
raise ArgumentError, "#{key} is not supported" unless Mihari.emitter_to_class.key?(key)
|
|
289
|
+
|
|
290
|
+
Mihari.emitter_to_class[key]
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
#
|
|
294
|
+
# @return [Array<Mihari::Emitters::Base>]
|
|
295
|
+
#
|
|
296
|
+
def emitters
|
|
297
|
+
@emitters ||= data[:emitters].map(&:deep_dup).map do |params|
|
|
298
|
+
name = params[:emitter]
|
|
299
|
+
options = params[:options]
|
|
300
|
+
|
|
301
|
+
%i[emitter options].each { |key| params.delete key }
|
|
302
|
+
|
|
303
|
+
klass = get_emitter_class(name)
|
|
304
|
+
klass.new(rule: self, options: options, **params)
|
|
305
|
+
end.map do |emitter|
|
|
306
|
+
emitter.validate_configuration!
|
|
307
|
+
emitter
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
#
|
|
312
|
+
# Get enricher class
|
|
313
|
+
#
|
|
314
|
+
# @param [String] key
|
|
315
|
+
#
|
|
316
|
+
# @return [Class<Mihari::Enrichers::Base>] enricher class
|
|
317
|
+
#
|
|
318
|
+
def get_enricher_class(key)
|
|
319
|
+
raise ArgumentError, "#{key} is not supported" unless Mihari.enricher_to_class.key?(key)
|
|
320
|
+
|
|
321
|
+
Mihari.enricher_to_class[key]
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
#
|
|
325
|
+
# @return [Array<Mihari::Enrichers::Base>] enrichers
|
|
326
|
+
#
|
|
327
|
+
def enrichers
|
|
328
|
+
@enrichers ||= data[:enrichers].map(&:deep_dup).map do |params|
|
|
329
|
+
name = params[:enricher]
|
|
330
|
+
options = params[:options]
|
|
331
|
+
|
|
332
|
+
%i[enricher options].each { |key| params.delete key }
|
|
333
|
+
|
|
334
|
+
klass = get_enricher_class(name)
|
|
335
|
+
klass.new(options: options, **params)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
#
|
|
340
|
+
# Validate the data format
|
|
341
|
+
#
|
|
342
|
+
def validate!
|
|
343
|
+
contract = Schemas::RuleContract.new
|
|
344
|
+
result = contract.call(data)
|
|
345
|
+
|
|
346
|
+
@data = result.to_h
|
|
347
|
+
@errors = result.errors
|
|
348
|
+
|
|
349
|
+
raise ValidationError.new("Validation failed", errors) if errors?
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|