mihari 4.1.1 → 4.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|