mihari 4.1.1 → 4.3.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/.github/workflows/test.yml +26 -4
- data/README.md +1 -1
- data/lib/mihari/analyzers/base.rb +18 -10
- data/lib/mihari/analyzers/rule.rb +50 -7
- data/lib/mihari/cli/base.rb +0 -4
- data/lib/mihari/commands/init.rb +1 -1
- data/lib/mihari/commands/search.rb +11 -58
- data/lib/mihari/commands/validator.rb +1 -2
- data/lib/mihari/constants.rb +2 -0
- data/lib/mihari/emitters/base.rb +8 -2
- data/lib/mihari/emitters/http.rb +127 -0
- data/lib/mihari/emitters/slack.rb +40 -4
- data/lib/mihari/emitters/webhook.rb +7 -16
- data/lib/mihari/enrichers/base.rb +5 -2
- data/lib/mihari/enrichers/ipinfo.rb +4 -3
- data/lib/mihari/{web/entities → entities}/alert.rb +0 -0
- data/lib/mihari/{web/entities → entities}/artifact.rb +0 -0
- data/lib/mihari/{web/entities → entities}/autonomous_system.rb +0 -0
- data/lib/mihari/{web/entities → entities}/command.rb +0 -0
- data/lib/mihari/{web/entities → entities}/config.rb +0 -0
- data/lib/mihari/{web/entities → entities}/dns.rb +0 -0
- data/lib/mihari/{web/entities → entities}/geolocation.rb +0 -0
- data/lib/mihari/{web/entities → entities}/ip_address.rb +0 -0
- data/lib/mihari/{web/entities → entities}/message.rb +0 -0
- data/lib/mihari/{web/entities → entities}/reverse_dns.rb +0 -0
- data/lib/mihari/{web/entities → entities}/rule.rb +5 -0
- data/lib/mihari/{web/entities → entities}/source.rb +0 -0
- data/lib/mihari/{web/entities → entities}/tag.rb +0 -0
- data/lib/mihari/{web/entities → entities}/whois.rb +0 -0
- data/lib/mihari/errors.rb +2 -0
- data/lib/mihari/feed/reader.rb +16 -58
- data/lib/mihari/http.rb +99 -0
- data/lib/mihari/mixins/error_notification.rb +20 -0
- data/lib/mihari/mixins/retriable.rb +12 -2
- data/lib/mihari/mixins/rule.rb +1 -2
- data/lib/mihari/schemas/rule.rb +30 -4
- data/lib/mihari/structs/ipinfo.rb +2 -3
- data/lib/mihari/structs/rule.rb +31 -0
- data/lib/mihari/structs/shodan.rb +9 -1
- data/lib/mihari/types.rb +11 -3
- data/lib/mihari/version.rb +1 -1
- data/lib/mihari/web/api.rb +0 -20
- data/lib/mihari/web/app.rb +2 -2
- data/lib/mihari/web/endpoints/rules.rb +3 -1
- data/lib/mihari/web/middleware/error_notification_adapter.rb +19 -0
- data/lib/mihari/web/public/index.html +1 -1
- data/lib/mihari/web/public/redoc-static.html +1888 -166
- data/lib/mihari/web/public/static/css/app.0de4b715.css +1 -0
- data/lib/mihari/web/public/static/css/app.43138058.css +1 -0
- data/lib/mihari/web/public/static/css/chunk-vendors.3ed9b08e.css +7 -0
- data/lib/mihari/web/public/static/css/chunk-vendors.c57bb3fd.css +7 -0
- data/lib/mihari/web/public/static/fonts/fa-brands-400.1fd0b4d7.ttf +0 -0
- data/lib/mihari/web/public/static/fonts/fa-brands-400.5d5236fb.woff2 +0 -0
- data/lib/mihari/web/public/static/fonts/fa-brands-400.edf40f86.woff2 +0 -0
- data/lib/mihari/web/public/static/fonts/fa-brands-400.f7223235.ttf +0 -0
- data/lib/mihari/web/public/static/fonts/fa-regular-400.3665ebc7.woff2 +0 -0
- data/lib/mihari/web/public/static/fonts/fa-regular-400.64b3730e.woff2 +0 -0
- data/lib/mihari/web/public/static/fonts/fa-regular-400.95a8a8af.ttf +0 -0
- data/lib/mihari/web/public/static/fonts/fa-regular-400.a7fde52b.ttf +0 -0
- data/lib/mihari/web/public/static/fonts/fa-solid-900.0d2abd43.woff2 +0 -0
- data/lib/mihari/web/public/static/fonts/fa-solid-900.5b03221c.ttf +0 -0
- data/lib/mihari/web/public/static/fonts/fa-solid-900.6115ad71.woff2 +0 -0
- data/lib/mihari/web/public/static/fonts/fa-solid-900.f0203cfc.ttf +0 -0
- data/lib/mihari/web/public/static/fonts/fa-v4compatibility.42932bea.ttf +0 -0
- data/lib/mihari/web/public/static/fonts/fa-v4compatibility.e1023515.ttf +0 -0
- data/lib/mihari/web/public/static/js/app-legacy.46b666f0.js +2 -0
- data/lib/mihari/web/public/static/js/app-legacy.46b666f0.js.map +1 -0
- data/lib/mihari/web/public/static/js/app-legacy.e451304b.js +2 -0
- data/lib/mihari/web/public/static/js/app-legacy.e451304b.js.map +1 -0
- data/lib/mihari/web/public/static/js/app.4818aedd.js +2 -0
- data/lib/mihari/web/public/static/js/app.4818aedd.js.map +1 -0
- data/lib/mihari/web/public/static/js/app.e74e91d7.js +2 -0
- data/lib/mihari/web/public/static/js/app.e74e91d7.js.map +1 -0
- data/lib/mihari/web/public/static/js/chunk-vendors-legacy.41357cdf.js +25 -0
- data/lib/mihari/web/public/static/js/chunk-vendors-legacy.41357cdf.js.map +1 -0
- data/lib/mihari/web/public/static/js/chunk-vendors-legacy.c99e452e.js +17 -0
- data/lib/mihari/web/public/static/js/chunk-vendors-legacy.c99e452e.js.map +1 -0
- data/lib/mihari/web/public/static/js/chunk-vendors.15e84e22.js +23 -0
- data/lib/mihari/web/public/static/js/chunk-vendors.15e84e22.js.map +1 -0
- data/lib/mihari/web/public/static/js/chunk-vendors.c5525f1e.js +31 -0
- data/lib/mihari/web/public/static/js/chunk-vendors.c5525f1e.js.map +1 -0
- data/lib/mihari.rb +71 -21
- data/mihari.gemspec +16 -11
- data/sig/lib/mihari/constants.rbs +2 -0
- data/sig/lib/mihari/emitters/http.rbs +35 -0
- data/sig/lib/mihari/emitters/slack.rbs +29 -1
- data/sig/lib/mihari/feed/reader.rbs +2 -2
- data/sig/lib/mihari/http.rbs +64 -0
- data/sig/lib/mihari/mixins/error_notification.rbs +12 -0
- data/sig/lib/mihari/structs/rule.rbs +4 -0
- data/sig/lib/mihari/types.rbs +2 -0
- data/sig/lib/mihari.rbs +4 -8
- metadata +137 -62
- data/lib/mihari/cli/mixins/utils.rb +0 -72
- data/lib/mihari/emitters/stdout.rb +0 -22
- data/lib/mihari/notifiers/base.rb +0 -24
- data/lib/mihari/notifiers/exception_notifier.rb +0 -126
- data/lib/mihari/notifiers/slack.rb +0 -63
- data/sig/lib/mihari/cli/mixins/utils.rbs +0 -50
- data/sig/lib/mihari/notifiers/base.rbs +0 -18
- data/sig/lib/mihari/notifiers/exception_notifier.rbs +0 -75
- data/sig/lib/mihari/notifiers/slack.rbs +0 -50
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dc5b6ab7daa3030a0ef0778df2753247bde10b978be709102ecf1be60b672a9c
|
|
4
|
+
data.tar.gz: 1c1c283cf1e2989e9e94e0aa582567dd71b7900f5c5faa40a0ce3f1b327982a3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 918ee19022035b5f5e3db9a00318253803b1cc430b45676225af94861fe6dc6fe51343545d224e6094f09ad37dc003713fbbcb1777904b01205903e22d05bf23
|
|
7
|
+
data.tar.gz: c5ce99eb3d8e01b1b2a8ac51916afa172c1fecb55611a3b8aa76e686c72801beb11135ed1c8aed31e11b693b7467dbe513902f55e229c834bbfcb09460ba4202
|
data/.github/workflows/test.yml
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
name: Ruby CI
|
|
2
2
|
|
|
3
|
-
on:
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [master]
|
|
4
8
|
|
|
5
9
|
jobs:
|
|
6
|
-
|
|
10
|
+
test:
|
|
7
11
|
runs-on: ubuntu-latest
|
|
8
12
|
|
|
9
13
|
services:
|
|
@@ -39,10 +43,10 @@ jobs:
|
|
|
39
43
|
strategy:
|
|
40
44
|
fail-fast: false
|
|
41
45
|
matrix:
|
|
42
|
-
ruby: [2.7, "3.0"]
|
|
46
|
+
ruby: [2.7, "3.0", 3.1]
|
|
43
47
|
|
|
44
48
|
steps:
|
|
45
|
-
- uses: actions/checkout@
|
|
49
|
+
- uses: actions/checkout@v3
|
|
46
50
|
|
|
47
51
|
- name: Install dependencies
|
|
48
52
|
run: |
|
|
@@ -65,3 +69,21 @@ jobs:
|
|
|
65
69
|
DATABASE: mysql2://mysql:mysql@127.0.0.1:3306/test
|
|
66
70
|
run: |
|
|
67
71
|
bundle exec rake
|
|
72
|
+
|
|
73
|
+
- name: Coveralls Parallel
|
|
74
|
+
uses: coverallsapp/github-action@master
|
|
75
|
+
with:
|
|
76
|
+
github-token: ${{ secrets.github_token }}
|
|
77
|
+
flag-name: run-${{ matrix.ruby-version }}
|
|
78
|
+
parallel: true
|
|
79
|
+
|
|
80
|
+
coverage:
|
|
81
|
+
name: Coverage
|
|
82
|
+
needs: test
|
|
83
|
+
runs-on: ubuntu-latest
|
|
84
|
+
steps:
|
|
85
|
+
- name: Coveralls Finished
|
|
86
|
+
uses: coverallsapp/github-action@master
|
|
87
|
+
with:
|
|
88
|
+
github-token: ${{ secrets.github_token }}
|
|
89
|
+
parallel-finished: true
|
data/README.md
CHANGED
|
@@ -47,15 +47,23 @@ module Mihari
|
|
|
47
47
|
#
|
|
48
48
|
# Set artifacts & run emitters in parallel
|
|
49
49
|
#
|
|
50
|
-
# @return [nil]
|
|
50
|
+
# @return [Mihari::Alert, nil]
|
|
51
51
|
#
|
|
52
52
|
def run
|
|
53
|
+
unless configured?
|
|
54
|
+
class_name = self.class.to_s.split("::").last
|
|
55
|
+
raise ConfigurationError, "#{class_name} is not configured correctly"
|
|
56
|
+
end
|
|
57
|
+
|
|
53
58
|
with_db_connection do
|
|
54
59
|
set_enriched_artifacts
|
|
55
60
|
|
|
56
|
-
Parallel.
|
|
61
|
+
responses = Parallel.map(valid_emitters) do |emitter|
|
|
57
62
|
run_emitter emitter
|
|
58
63
|
end
|
|
64
|
+
|
|
65
|
+
# returns Mihari::Alert created by the database emitter
|
|
66
|
+
responses.find { |res| res.is_a?(Mihari::Alert) }
|
|
59
67
|
end
|
|
60
68
|
end
|
|
61
69
|
|
|
@@ -69,23 +77,26 @@ module Mihari
|
|
|
69
77
|
def run_emitter(emitter)
|
|
70
78
|
emitter.run(title: title, description: description, artifacts: enriched_artifacts, source: source, tags: tags)
|
|
71
79
|
rescue StandardError => e
|
|
72
|
-
|
|
80
|
+
Mihari.logger.info "Emission by #{emitter.class} is failed: #{e}"
|
|
73
81
|
end
|
|
74
82
|
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
class << self
|
|
84
|
+
def inherited(child)
|
|
85
|
+
super
|
|
86
|
+
Mihari.analyzers << child
|
|
87
|
+
end
|
|
77
88
|
end
|
|
78
89
|
|
|
79
90
|
#
|
|
80
91
|
# Normalize artifacts
|
|
81
|
-
# - Uniquefy artifacts by native #uniq
|
|
82
92
|
# - Convert data (string) into an artifact
|
|
83
93
|
# - Reject an invalid artifact
|
|
94
|
+
# - Uniquefy artifacts by data
|
|
84
95
|
#
|
|
85
96
|
# @return [Array<Mihari::Artifact>]
|
|
86
97
|
#
|
|
87
98
|
def normalized_artifacts
|
|
88
|
-
@normalized_artifacts ||= artifacts.compact.
|
|
99
|
+
@normalized_artifacts ||= artifacts.compact.sort.map do |artifact|
|
|
89
100
|
# No need to set data_type manually
|
|
90
101
|
# It is set automatically in #initialize
|
|
91
102
|
artifact.is_a?(Artifact) ? artifact : Artifact.new(data: artifact, source: source)
|
|
@@ -124,9 +135,6 @@ module Mihari
|
|
|
124
135
|
#
|
|
125
136
|
def set_enriched_artifacts
|
|
126
137
|
retry_on_error { enriched_artifacts }
|
|
127
|
-
rescue ArgumentError => e
|
|
128
|
-
klass = self.class.to_s.split("::").last.to_s
|
|
129
|
-
raise Error, "Please configure #{klass} settings properly. (#{e})"
|
|
130
138
|
end
|
|
131
139
|
|
|
132
140
|
#
|
|
@@ -28,6 +28,15 @@ module Mihari
|
|
|
28
28
|
"zoomeye" => ZoomEye
|
|
29
29
|
}.freeze
|
|
30
30
|
|
|
31
|
+
EMITTER_TO_CLASS = {
|
|
32
|
+
"database" => Emitters::Database,
|
|
33
|
+
"http" => Emitters::HTTP,
|
|
34
|
+
"misp" => Emitters::MISP,
|
|
35
|
+
"slack" => Emitters::Slack,
|
|
36
|
+
"the_hive" => Emitters::TheHive,
|
|
37
|
+
"webhook" => Emitters::Webhook
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
31
40
|
class Rule < Base
|
|
32
41
|
include Mixins::DisallowedDataValue
|
|
33
42
|
include Mixins::Rule
|
|
@@ -41,6 +50,8 @@ module Mihari
|
|
|
41
50
|
option :allowed_data_types, default: proc { ALLOWED_DATA_TYPES }
|
|
42
51
|
option :disallowed_data_values, default: proc { [] }
|
|
43
52
|
|
|
53
|
+
option :emitters, optional: true
|
|
54
|
+
|
|
44
55
|
attr_reader :source
|
|
45
56
|
|
|
46
57
|
def initialize(**kwargs)
|
|
@@ -48,6 +59,8 @@ module Mihari
|
|
|
48
59
|
|
|
49
60
|
@source = id
|
|
50
61
|
|
|
62
|
+
@emitters = emitters || DEFAULT_EMITTERS
|
|
63
|
+
|
|
51
64
|
validate_analyzer_configurations
|
|
52
65
|
end
|
|
53
66
|
|
|
@@ -59,18 +72,20 @@ module Mihari
|
|
|
59
72
|
def artifacts
|
|
60
73
|
artifacts = []
|
|
61
74
|
|
|
62
|
-
queries.each do |
|
|
63
|
-
|
|
75
|
+
queries.each do |original_params|
|
|
76
|
+
parmas = original_params.deep_dup
|
|
77
|
+
|
|
78
|
+
analyzer_name = parmas[:analyzer]
|
|
64
79
|
klass = get_analyzer_class(analyzer_name)
|
|
65
80
|
|
|
66
|
-
query =
|
|
81
|
+
query = parmas[:query]
|
|
67
82
|
|
|
68
83
|
# set interval in the top level
|
|
69
|
-
options =
|
|
84
|
+
options = parmas[:options] || {}
|
|
70
85
|
interval = options[:interval]
|
|
71
|
-
|
|
86
|
+
parmas[:interval] = interval if interval
|
|
72
87
|
|
|
73
|
-
analyzer = klass.new(query, **
|
|
88
|
+
analyzer = klass.new(query, **parmas)
|
|
74
89
|
|
|
75
90
|
# Use #normalized_artifacts method to get atrifacts as Array<Mihari::Artifact>
|
|
76
91
|
# So Mihari::Artifact object has "source" attribute (e.g. "Shodan")
|
|
@@ -120,6 +135,34 @@ module Mihari
|
|
|
120
135
|
|
|
121
136
|
private
|
|
122
137
|
|
|
138
|
+
#
|
|
139
|
+
# Get emitter class
|
|
140
|
+
#
|
|
141
|
+
# @param [String] emitter_name
|
|
142
|
+
#
|
|
143
|
+
# @return [Class<Mihari::Emitters::Base>] emitter class
|
|
144
|
+
#
|
|
145
|
+
def get_emitter_class(emitter_name)
|
|
146
|
+
emitter = EMITTER_TO_CLASS[emitter_name]
|
|
147
|
+
return emitter if emitter
|
|
148
|
+
|
|
149
|
+
raise ArgumentError, "#{emitter_name} is not supported"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def valid_emitters
|
|
153
|
+
@valid_emitters ||= emitters.filter_map do |original_params|
|
|
154
|
+
params = original_params.deep_dup
|
|
155
|
+
|
|
156
|
+
name = params[:emitter]
|
|
157
|
+
params.delete(:emitter)
|
|
158
|
+
|
|
159
|
+
klass = get_emitter_class(name)
|
|
160
|
+
emitter = klass.new(**params)
|
|
161
|
+
|
|
162
|
+
emitter.valid? ? emitter : nil
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
123
166
|
#
|
|
124
167
|
# Get analyzer class
|
|
125
168
|
#
|
|
@@ -145,7 +188,7 @@ module Mihari
|
|
|
145
188
|
instance = klass.new("dummy")
|
|
146
189
|
unless instance.configured?
|
|
147
190
|
klass_name = klass.to_s.split("::").last
|
|
148
|
-
raise
|
|
191
|
+
raise ConfigurationError, "#{klass_name} is not configured correctly"
|
|
149
192
|
end
|
|
150
193
|
end
|
|
151
194
|
end
|
data/lib/mihari/cli/base.rb
CHANGED
data/lib/mihari/commands/init.rb
CHANGED
|
@@ -5,12 +5,14 @@ module Mihari
|
|
|
5
5
|
module Search
|
|
6
6
|
include Mixins::Database
|
|
7
7
|
include Mixins::Rule
|
|
8
|
+
include Mixins::ErrorNotification
|
|
8
9
|
|
|
9
10
|
def self.included(thor)
|
|
10
11
|
thor.class_eval do
|
|
11
12
|
desc "search [RULE]", "Search by a rule"
|
|
12
13
|
def search_by_rule(path_or_id)
|
|
13
14
|
rule = load_rule(path_or_id)
|
|
15
|
+
|
|
14
16
|
# validate
|
|
15
17
|
begin
|
|
16
18
|
validate_rule! rule
|
|
@@ -18,22 +20,17 @@ module Mihari
|
|
|
18
20
|
raise e
|
|
19
21
|
end
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
analyzer = build_rule_analyzer(
|
|
23
|
-
title: rule[:title],
|
|
24
|
-
description: rule[:description],
|
|
25
|
-
queries: rule[:queries],
|
|
26
|
-
tags: rule[:tags],
|
|
27
|
-
allowed_data_types: rule[:allowed_data_types],
|
|
28
|
-
disallowed_data_values: rule[:disallowed_data_values],
|
|
29
|
-
id: rule.id
|
|
30
|
-
)
|
|
23
|
+
analyzer = rule.to_analyzer
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
with_error_notification do
|
|
26
|
+
alert = analyzer.run
|
|
34
27
|
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
if alert
|
|
29
|
+
data = Mihari::Entities::Alert.represent(alert)
|
|
30
|
+
puts JSON.pretty_generate(data.as_json)
|
|
31
|
+
else
|
|
32
|
+
Mihari.logger.info "There is no new artifact"
|
|
33
|
+
end
|
|
37
34
|
|
|
38
35
|
# record a rule
|
|
39
36
|
with_db_connection do
|
|
@@ -46,50 +43,6 @@ module Mihari
|
|
|
46
43
|
end
|
|
47
44
|
end
|
|
48
45
|
end
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
#
|
|
53
|
-
# Build a rule analyzer
|
|
54
|
-
#
|
|
55
|
-
# @param [String] title
|
|
56
|
-
# @param [String] description
|
|
57
|
-
# @param [Array<Hash>] queries
|
|
58
|
-
# @param [Array<String>, nil] tags
|
|
59
|
-
# @param [Array<String>, nil] allowed_data_types
|
|
60
|
-
# @param [Array<String>, nil] disallowed_data_values
|
|
61
|
-
#
|
|
62
|
-
# @return [Mihari::Analyzers::Rule]
|
|
63
|
-
#
|
|
64
|
-
def build_rule_analyzer(title:, description:, queries:, tags: nil, allowed_data_types: nil, disallowed_data_values: nil, id: nil)
|
|
65
|
-
tags = [] if tags.nil?
|
|
66
|
-
allowed_data_types = ALLOWED_DATA_TYPES if allowed_data_types.nil?
|
|
67
|
-
disallowed_data_values = [] if disallowed_data_values.nil?
|
|
68
|
-
|
|
69
|
-
Analyzers::Rule.new(
|
|
70
|
-
title: title,
|
|
71
|
-
description: description,
|
|
72
|
-
tags: tags,
|
|
73
|
-
queries: queries,
|
|
74
|
-
allowed_data_types: allowed_data_types,
|
|
75
|
-
disallowed_data_values: disallowed_data_values,
|
|
76
|
-
id: id
|
|
77
|
-
)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
#
|
|
81
|
-
# Run rule analyzer
|
|
82
|
-
#
|
|
83
|
-
# @param [Mihari::Analyzer::Rule] analyzer
|
|
84
|
-
#
|
|
85
|
-
# @return [nil]
|
|
86
|
-
#
|
|
87
|
-
def run_rule_analyzer(analyzer, ignore_old_artifacts: false, ignore_threshold: 0)
|
|
88
|
-
analyzer.ignore_old_artifacts = ignore_old_artifacts
|
|
89
|
-
analyzer.ignore_threshold = ignore_threshold
|
|
90
|
-
|
|
91
|
-
analyzer.run
|
|
92
|
-
end
|
|
93
46
|
end
|
|
94
47
|
end
|
|
95
48
|
end
|
|
@@ -13,8 +13,7 @@ module Mihari
|
|
|
13
13
|
|
|
14
14
|
begin
|
|
15
15
|
validate_rule! rule
|
|
16
|
-
|
|
17
|
-
puts rule.data.to_yaml
|
|
16
|
+
Mihari.logger.info "Valid format. The input is parsed as the following:\n#{rule.data.to_yaml}"
|
|
18
17
|
rescue RuleValidationError
|
|
19
18
|
nil
|
|
20
19
|
end
|
data/lib/mihari/constants.rb
CHANGED
data/lib/mihari/emitters/base.rb
CHANGED
|
@@ -6,8 +6,14 @@ module Mihari
|
|
|
6
6
|
include Mixins::Configurable
|
|
7
7
|
include Mixins::Retriable
|
|
8
8
|
|
|
9
|
-
def
|
|
10
|
-
|
|
9
|
+
def initialize(*)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def inherited(child)
|
|
14
|
+
super
|
|
15
|
+
Mihari.emitters << child
|
|
16
|
+
end
|
|
11
17
|
end
|
|
12
18
|
|
|
13
19
|
# @return [Boolean]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
|
|
5
|
+
module Mihari
|
|
6
|
+
module Emitters
|
|
7
|
+
class PayloadTemplate < ERB
|
|
8
|
+
def self.template
|
|
9
|
+
%{
|
|
10
|
+
{
|
|
11
|
+
"title": "<%= @title %>",
|
|
12
|
+
"description": "<%= @description %>",
|
|
13
|
+
"source": "<%= @source %>",
|
|
14
|
+
"artifacts": [
|
|
15
|
+
<% @artifacts.each_with_index do |artifact, idx| %>
|
|
16
|
+
"<%= artifact.data %>"
|
|
17
|
+
<%= ',' if idx < (@artifacts.length - 1) %>
|
|
18
|
+
<% end %>
|
|
19
|
+
],
|
|
20
|
+
"tags": [
|
|
21
|
+
<% @tags.each_with_index do |tag, idx| %>
|
|
22
|
+
"<%= tag %>"
|
|
23
|
+
<%= ',' if idx < (@tags.length - 1) %>
|
|
24
|
+
<% end %>
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(title:, description:, artifacts:, source:, tags:, options: {})
|
|
31
|
+
@title = title
|
|
32
|
+
@description = description
|
|
33
|
+
@artifacts = artifacts
|
|
34
|
+
@source = source
|
|
35
|
+
@tags = tags
|
|
36
|
+
|
|
37
|
+
@template = options.fetch(:template, self.class.template)
|
|
38
|
+
super(@template)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def result
|
|
42
|
+
super(binding)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class HTTP < Base
|
|
47
|
+
# @return [Addressable::URI, nil]
|
|
48
|
+
attr_reader :uri
|
|
49
|
+
|
|
50
|
+
# @return [Hash]
|
|
51
|
+
attr_reader :http_request_headers
|
|
52
|
+
|
|
53
|
+
# @return [String]
|
|
54
|
+
attr_reader :http_request_method
|
|
55
|
+
|
|
56
|
+
# @return [String, nil]
|
|
57
|
+
attr_reader :template
|
|
58
|
+
|
|
59
|
+
def initialize(*args, **kwargs)
|
|
60
|
+
super(*args, **kwargs)
|
|
61
|
+
|
|
62
|
+
uri = kwargs[:url] || kwargs[:uri]
|
|
63
|
+
http_request_headers = kwargs[:http_request_headers] || {}
|
|
64
|
+
http_request_method = kwargs[:http_request_method] || "POST"
|
|
65
|
+
template = kwargs[:template]
|
|
66
|
+
|
|
67
|
+
@uri = Addressable::URI.parse(uri) if uri
|
|
68
|
+
@http_request_headers = http_request_headers
|
|
69
|
+
@http_request_method = http_request_method
|
|
70
|
+
@template = template
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def emit(title:, description:, artifacts:, source:, tags:)
|
|
74
|
+
return if artifacts.empty?
|
|
75
|
+
|
|
76
|
+
res = nil
|
|
77
|
+
|
|
78
|
+
payload_ = payload_as_string(
|
|
79
|
+
title: title,
|
|
80
|
+
description: description,
|
|
81
|
+
artifacts: artifacts,
|
|
82
|
+
source: source,
|
|
83
|
+
tags: tags
|
|
84
|
+
)
|
|
85
|
+
payload = JSON.parse(payload_)
|
|
86
|
+
|
|
87
|
+
client = Mihari::HTTP.new(uri, headers: http_request_headers, payload: payload)
|
|
88
|
+
|
|
89
|
+
case http_request_method
|
|
90
|
+
when "GET"
|
|
91
|
+
res = client.get
|
|
92
|
+
when "POST"
|
|
93
|
+
res = client.post
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
res
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def valid?
|
|
100
|
+
return false if uri.nil?
|
|
101
|
+
|
|
102
|
+
["http", "https"].include? uri.scheme.downcase
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def payload_as_string(title:, description:, artifacts:, source:, tags:)
|
|
108
|
+
@payload_as_string ||= [].tap do |out|
|
|
109
|
+
options = {}
|
|
110
|
+
unless template.nil?
|
|
111
|
+
options[:template] = File.read(template)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
payload_template = PayloadTemplate.new(
|
|
115
|
+
title: title,
|
|
116
|
+
description: description,
|
|
117
|
+
artifacts: artifacts,
|
|
118
|
+
source: source,
|
|
119
|
+
tags: tags,
|
|
120
|
+
options: options
|
|
121
|
+
)
|
|
122
|
+
out << payload_template.result
|
|
123
|
+
end.first
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -109,12 +109,48 @@ module Mihari
|
|
|
109
109
|
end
|
|
110
110
|
|
|
111
111
|
class Slack < Base
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
SLACK_WEBHOOK_URL_KEY = "SLACK_WEBHOOK_URL"
|
|
113
|
+
SLACK_CHANNEL_KEY = "SLACK_CHANNEL"
|
|
114
|
+
DEFAULT_USERNAME = "mihari"
|
|
115
|
+
|
|
116
|
+
#
|
|
117
|
+
# Slack channel to post
|
|
118
|
+
#
|
|
119
|
+
# @return [String]
|
|
120
|
+
#
|
|
121
|
+
def slack_channel
|
|
122
|
+
Mihari.config.slack_channel || "#general"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
#
|
|
126
|
+
# Slack webhook URL
|
|
127
|
+
#
|
|
128
|
+
# @return [String]
|
|
129
|
+
#
|
|
130
|
+
def slack_webhook_url
|
|
131
|
+
Mihari.config.slack_webhook_url
|
|
114
132
|
end
|
|
115
133
|
|
|
134
|
+
#
|
|
135
|
+
# Check Slack webhook URL is set
|
|
136
|
+
#
|
|
137
|
+
# @return [Boolean]
|
|
138
|
+
#
|
|
139
|
+
def slack_webhook_url?
|
|
140
|
+
!Mihari.config.slack_webhook_url.nil?
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
#
|
|
144
|
+
# Check Slack webhook URL is set. Alias of #slack_webhook_url?.
|
|
145
|
+
#
|
|
146
|
+
# @return [Boolean]
|
|
147
|
+
#
|
|
116
148
|
def valid?
|
|
117
|
-
|
|
149
|
+
slack_webhook_url?
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def notifier
|
|
153
|
+
@notifier ||= ::Slack::Notifier.new(slack_webhook_url, channel: slack_channel, username: DEFAULT_USERNAME)
|
|
118
154
|
end
|
|
119
155
|
|
|
120
156
|
#
|
|
@@ -155,7 +191,7 @@ module Mihari
|
|
|
155
191
|
attachments = to_attachments(artifacts)
|
|
156
192
|
text = to_text(title: title, description: description, tags: tags)
|
|
157
193
|
|
|
158
|
-
notifier.
|
|
194
|
+
notifier.post(text: text, attachments: attachments, mrkdwn: true)
|
|
159
195
|
end
|
|
160
196
|
|
|
161
197
|
private
|
|
@@ -11,20 +11,11 @@ module Mihari
|
|
|
11
11
|
def emit(title:, description:, artifacts:, source:, tags:)
|
|
12
12
|
return if artifacts.empty?
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
title: title,
|
|
17
|
-
description: description,
|
|
18
|
-
artifacts: artifacts.map(&:data),
|
|
19
|
-
source: source,
|
|
20
|
-
tags: tags
|
|
21
|
-
}
|
|
14
|
+
headers = { 'content-type': "application/x-www-form-urlencoded" }
|
|
15
|
+
headers["content-type"] = "application/json" if use_json_body?
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
else
|
|
26
|
-
Net::HTTP.post_form(uri, data)
|
|
27
|
-
end
|
|
17
|
+
emitter = Emitters::HTTP.new(uri: Mihari.config.webhook_url)
|
|
18
|
+
emitter.emit(title: title, description: description, artifacts: artifacts, source: source, tags: tags)
|
|
28
19
|
end
|
|
29
20
|
|
|
30
21
|
private
|
|
@@ -45,16 +36,16 @@ module Mihari
|
|
|
45
36
|
#
|
|
46
37
|
# Check whether a webhook URL is set or not
|
|
47
38
|
#
|
|
48
|
-
# @return [
|
|
39
|
+
# @return [Boolean]
|
|
49
40
|
#
|
|
50
41
|
def webhook_url?
|
|
51
42
|
!webhook_url.nil?
|
|
52
43
|
end
|
|
53
44
|
|
|
54
45
|
#
|
|
55
|
-
# Check whether to use JSON body or
|
|
46
|
+
# Check whether to use JSON body or not
|
|
56
47
|
#
|
|
57
|
-
# @return [
|
|
48
|
+
# @return [Boolean]
|
|
58
49
|
#
|
|
59
50
|
def use_json_body?
|
|
60
51
|
@use_json_body ||= Mihari.config.webhook_use_json_body
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "net/https"
|
|
4
4
|
|
|
5
5
|
module Mihari
|
|
6
6
|
module Enrichers
|
|
@@ -34,11 +34,12 @@ module Mihari
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
begin
|
|
37
|
-
|
|
37
|
+
url = "https://ipinfo.io/#{ip}/json"
|
|
38
|
+
res = HTTP.get(url, headers: headers)
|
|
38
39
|
data = JSON.parse(res.body.to_s)
|
|
39
40
|
|
|
40
41
|
Structs::IPInfo::Response.from_dynamic! data
|
|
41
|
-
rescue
|
|
42
|
+
rescue HttpError
|
|
42
43
|
nil
|
|
43
44
|
end
|
|
44
45
|
end
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|