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.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +26 -4
  3. data/README.md +1 -1
  4. data/lib/mihari/analyzers/base.rb +18 -10
  5. data/lib/mihari/analyzers/rule.rb +50 -7
  6. data/lib/mihari/cli/base.rb +0 -4
  7. data/lib/mihari/commands/init.rb +1 -1
  8. data/lib/mihari/commands/search.rb +11 -58
  9. data/lib/mihari/commands/validator.rb +1 -2
  10. data/lib/mihari/constants.rb +2 -0
  11. data/lib/mihari/emitters/base.rb +8 -2
  12. data/lib/mihari/emitters/http.rb +127 -0
  13. data/lib/mihari/emitters/slack.rb +40 -4
  14. data/lib/mihari/emitters/webhook.rb +7 -16
  15. data/lib/mihari/enrichers/base.rb +5 -2
  16. data/lib/mihari/enrichers/ipinfo.rb +4 -3
  17. data/lib/mihari/{web/entities → entities}/alert.rb +0 -0
  18. data/lib/mihari/{web/entities → entities}/artifact.rb +0 -0
  19. data/lib/mihari/{web/entities → entities}/autonomous_system.rb +0 -0
  20. data/lib/mihari/{web/entities → entities}/command.rb +0 -0
  21. data/lib/mihari/{web/entities → entities}/config.rb +0 -0
  22. data/lib/mihari/{web/entities → entities}/dns.rb +0 -0
  23. data/lib/mihari/{web/entities → entities}/geolocation.rb +0 -0
  24. data/lib/mihari/{web/entities → entities}/ip_address.rb +0 -0
  25. data/lib/mihari/{web/entities → entities}/message.rb +0 -0
  26. data/lib/mihari/{web/entities → entities}/reverse_dns.rb +0 -0
  27. data/lib/mihari/{web/entities → entities}/rule.rb +5 -0
  28. data/lib/mihari/{web/entities → entities}/source.rb +0 -0
  29. data/lib/mihari/{web/entities → entities}/tag.rb +0 -0
  30. data/lib/mihari/{web/entities → entities}/whois.rb +0 -0
  31. data/lib/mihari/errors.rb +2 -0
  32. data/lib/mihari/feed/reader.rb +16 -58
  33. data/lib/mihari/http.rb +99 -0
  34. data/lib/mihari/mixins/error_notification.rb +20 -0
  35. data/lib/mihari/mixins/retriable.rb +12 -2
  36. data/lib/mihari/mixins/rule.rb +1 -2
  37. data/lib/mihari/schemas/rule.rb +30 -4
  38. data/lib/mihari/structs/ipinfo.rb +2 -3
  39. data/lib/mihari/structs/rule.rb +31 -0
  40. data/lib/mihari/structs/shodan.rb +9 -1
  41. data/lib/mihari/types.rb +11 -3
  42. data/lib/mihari/version.rb +1 -1
  43. data/lib/mihari/web/api.rb +0 -20
  44. data/lib/mihari/web/app.rb +2 -2
  45. data/lib/mihari/web/endpoints/rules.rb +3 -1
  46. data/lib/mihari/web/middleware/error_notification_adapter.rb +19 -0
  47. data/lib/mihari/web/public/index.html +1 -1
  48. data/lib/mihari/web/public/redoc-static.html +1888 -166
  49. data/lib/mihari/web/public/static/css/app.0de4b715.css +1 -0
  50. data/lib/mihari/web/public/static/css/app.43138058.css +1 -0
  51. data/lib/mihari/web/public/static/css/chunk-vendors.3ed9b08e.css +7 -0
  52. data/lib/mihari/web/public/static/css/chunk-vendors.c57bb3fd.css +7 -0
  53. data/lib/mihari/web/public/static/fonts/fa-brands-400.1fd0b4d7.ttf +0 -0
  54. data/lib/mihari/web/public/static/fonts/fa-brands-400.5d5236fb.woff2 +0 -0
  55. data/lib/mihari/web/public/static/fonts/fa-brands-400.edf40f86.woff2 +0 -0
  56. data/lib/mihari/web/public/static/fonts/fa-brands-400.f7223235.ttf +0 -0
  57. data/lib/mihari/web/public/static/fonts/fa-regular-400.3665ebc7.woff2 +0 -0
  58. data/lib/mihari/web/public/static/fonts/fa-regular-400.64b3730e.woff2 +0 -0
  59. data/lib/mihari/web/public/static/fonts/fa-regular-400.95a8a8af.ttf +0 -0
  60. data/lib/mihari/web/public/static/fonts/fa-regular-400.a7fde52b.ttf +0 -0
  61. data/lib/mihari/web/public/static/fonts/fa-solid-900.0d2abd43.woff2 +0 -0
  62. data/lib/mihari/web/public/static/fonts/fa-solid-900.5b03221c.ttf +0 -0
  63. data/lib/mihari/web/public/static/fonts/fa-solid-900.6115ad71.woff2 +0 -0
  64. data/lib/mihari/web/public/static/fonts/fa-solid-900.f0203cfc.ttf +0 -0
  65. data/lib/mihari/web/public/static/fonts/fa-v4compatibility.42932bea.ttf +0 -0
  66. data/lib/mihari/web/public/static/fonts/fa-v4compatibility.e1023515.ttf +0 -0
  67. data/lib/mihari/web/public/static/js/app-legacy.46b666f0.js +2 -0
  68. data/lib/mihari/web/public/static/js/app-legacy.46b666f0.js.map +1 -0
  69. data/lib/mihari/web/public/static/js/app-legacy.e451304b.js +2 -0
  70. data/lib/mihari/web/public/static/js/app-legacy.e451304b.js.map +1 -0
  71. data/lib/mihari/web/public/static/js/app.4818aedd.js +2 -0
  72. data/lib/mihari/web/public/static/js/app.4818aedd.js.map +1 -0
  73. data/lib/mihari/web/public/static/js/app.e74e91d7.js +2 -0
  74. data/lib/mihari/web/public/static/js/app.e74e91d7.js.map +1 -0
  75. data/lib/mihari/web/public/static/js/chunk-vendors-legacy.41357cdf.js +25 -0
  76. data/lib/mihari/web/public/static/js/chunk-vendors-legacy.41357cdf.js.map +1 -0
  77. data/lib/mihari/web/public/static/js/chunk-vendors-legacy.c99e452e.js +17 -0
  78. data/lib/mihari/web/public/static/js/chunk-vendors-legacy.c99e452e.js.map +1 -0
  79. data/lib/mihari/web/public/static/js/chunk-vendors.15e84e22.js +23 -0
  80. data/lib/mihari/web/public/static/js/chunk-vendors.15e84e22.js.map +1 -0
  81. data/lib/mihari/web/public/static/js/chunk-vendors.c5525f1e.js +31 -0
  82. data/lib/mihari/web/public/static/js/chunk-vendors.c5525f1e.js.map +1 -0
  83. data/lib/mihari.rb +71 -21
  84. data/mihari.gemspec +16 -11
  85. data/sig/lib/mihari/constants.rbs +2 -0
  86. data/sig/lib/mihari/emitters/http.rbs +35 -0
  87. data/sig/lib/mihari/emitters/slack.rbs +29 -1
  88. data/sig/lib/mihari/feed/reader.rbs +2 -2
  89. data/sig/lib/mihari/http.rbs +64 -0
  90. data/sig/lib/mihari/mixins/error_notification.rbs +12 -0
  91. data/sig/lib/mihari/structs/rule.rbs +4 -0
  92. data/sig/lib/mihari/types.rbs +2 -0
  93. data/sig/lib/mihari.rbs +4 -8
  94. metadata +137 -62
  95. data/lib/mihari/cli/mixins/utils.rb +0 -72
  96. data/lib/mihari/emitters/stdout.rb +0 -22
  97. data/lib/mihari/notifiers/base.rb +0 -24
  98. data/lib/mihari/notifiers/exception_notifier.rb +0 -126
  99. data/lib/mihari/notifiers/slack.rb +0 -63
  100. data/sig/lib/mihari/cli/mixins/utils.rbs +0 -50
  101. data/sig/lib/mihari/notifiers/base.rbs +0 -18
  102. data/sig/lib/mihari/notifiers/exception_notifier.rbs +0 -75
  103. data/sig/lib/mihari/notifiers/slack.rbs +0 -50
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02dcb4c10888bb90fd38fc6ae879eb54d1eea6e06fbe3c7c4f662a76271e85bc
4
- data.tar.gz: dd146a224d1b280f58ea71a566472a0dd80ad7e676703d6b6240c744ccad7244
3
+ metadata.gz: dc5b6ab7daa3030a0ef0778df2753247bde10b978be709102ecf1be60b672a9c
4
+ data.tar.gz: 1c1c283cf1e2989e9e94e0aa582567dd71b7900f5c5faa40a0ce3f1b327982a3
5
5
  SHA512:
6
- metadata.gz: b23693c762329a79dddfa92699cd55c3d6a566e7753fea87d41a854871eab6ca408491000c7dd944396ac32bab2468e9c00bfed81335f5f5253c701033a53a5e
7
- data.tar.gz: 3bc90185ef6a9684b05e9d7177a90aec54a73e5d0978e7d0f50c8f56a69e6040db1df567a98d7425ce2656e18e704d5f672fae1b44323c163f2ce913644493f2
6
+ metadata.gz: 918ee19022035b5f5e3db9a00318253803b1cc430b45676225af94861fe6dc6fe51343545d224e6094f09ad37dc003713fbbcb1777904b01205903e22d05bf23
7
+ data.tar.gz: c5ce99eb3d8e01b1b2a8ac51916afa172c1fecb55611a3b8aa76e686c72801beb11135ed1c8aed31e11b693b7467dbe513902f55e229c834bbfcb09460ba4202
@@ -1,9 +1,13 @@
1
1
  name: Ruby CI
2
2
 
3
- on: [pull_request]
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+ branches: [master]
4
8
 
5
9
  jobs:
6
- build:
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@v2
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
@@ -9,7 +9,7 @@
9
9
 
10
10
  [![](images/tines.png)](https://tines.io?utm_source=github&utm_medium=sponsorship&utm_campaign=ninoseki)
11
11
 
12
- Mihari is a framework for continuous OSINT based threat hunting.
12
+ Mihari is a tool for OSINT based threat hunting.
13
13
 
14
14
  ## How it works
15
15
 
@@ -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.each(valid_emitters) do |emitter|
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
- puts "Emission by #{emitter.class} is failed: #{e}"
80
+ Mihari.logger.info "Emission by #{emitter.class} is failed: #{e}"
73
81
  end
74
82
 
75
- def self.inherited(child)
76
- Mihari.analyzers << child
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.uniq.sort.map do |artifact|
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 |params|
63
- analyzer_name = params[:analyzer]
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 = params[:query]
81
+ query = parmas[:query]
67
82
 
68
83
  # set interval in the top level
69
- options = params[:options] || {}
84
+ options = parmas[:options] || {}
70
85
  interval = options[:interval]
71
- params[:interval] = interval if interval
86
+ parmas[:interval] = interval if interval
72
87
 
73
- analyzer = klass.new(query, **params)
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 ArgumentError, "#{klass_name} is not configured correctly"
191
+ raise ConfigurationError, "#{klass_name} is not configured correctly"
149
192
  end
150
193
  end
151
194
  end
@@ -1,12 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "mihari/cli/mixins/utils"
4
-
5
3
  module Mihari
6
4
  module CLI
7
5
  class Base < Thor
8
- include Mixins::Utils
9
-
10
6
  class << self
11
7
  def exit_on_failure?
12
8
  true
@@ -19,7 +19,7 @@ module Mihari
19
19
 
20
20
  initialize_rule_yaml filename
21
21
 
22
- puts "The rule file is initialized as #{filename}.".colorize(:blue)
22
+ Mihari.logger.info "The rule file is initialized as #{filename}."
23
23
  end
24
24
  end
25
25
  end
@@ -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
- # build and run the analyzer
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
- ignore_old_artifacts = rule[:ignore_old_artifacts]
33
- ignore_threshold = rule[:ignore_threshold]
25
+ with_error_notification do
26
+ alert = analyzer.run
34
27
 
35
- with_error_handling do
36
- run_rule_analyzer analyzer, ignore_old_artifacts: ignore_old_artifacts, ignore_threshold: ignore_threshold
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
- puts "Valid format. The input is parsed as the following:"
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
@@ -2,4 +2,6 @@
2
2
 
3
3
  module Mihari
4
4
  ALLOWED_DATA_TYPES = ["hash", "ip", "domain", "url", "mail"].freeze
5
+
6
+ DEFAULT_EMITTERS = ["database", "misp", "slack", "the_hive", "webhook"].map { |name| { emitter: name } }.freeze
5
7
  end
@@ -6,8 +6,14 @@ module Mihari
6
6
  include Mixins::Configurable
7
7
  include Mixins::Retriable
8
8
 
9
- def self.inherited(child)
10
- Mihari.emitters << child
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
- def notifier
113
- @notifier ||= Notifiers::Slack.new
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
- notifier.valid?
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.notify(text: text, attachments: attachments)
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
- uri = Addressable::URI.parse(Mihari.config.webhook_url)
15
- data = {
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
- if use_json_body?
24
- Net::HTTP.post(uri, data.to_json, "Content-Type" => "application/json")
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 [<Type>] <description>
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 NOT
46
+ # Check whether to use JSON body or not
56
47
  #
57
- # @return [<Type>] <description>
48
+ # @return [Boolean]
58
49
  #
59
50
  def use_json_body?
60
51
  @use_json_body ||= Mihari.config.webhook_use_json_body
@@ -5,8 +5,11 @@ module Mihari
5
5
  class Base
6
6
  include Mixins::Configurable
7
7
 
8
- def self.inherited(child)
9
- Mihari.enrichers << child
8
+ class << self
9
+ def inherited(child)
10
+ super
11
+ Mihari.enrichers << child
12
+ end
10
13
  end
11
14
 
12
15
  # @return [Boolean]
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "http"
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
- res = HTTP.headers(headers).get("https://ipinfo.io/#{ip}/json")
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 HTTP::Error
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