mihari 4.1.1 → 4.3.0

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