mihari 4.1.2 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +1 -1
  3. data/README.md +1 -1
  4. data/lib/mihari/analyzers/base.rb +18 -10
  5. data/lib/mihari/analyzers/rule.rb +1 -1
  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/emitters/base.rb +5 -2
  11. data/lib/mihari/emitters/slack.rb +40 -4
  12. data/lib/mihari/enrichers/base.rb +5 -2
  13. data/lib/mihari/enrichers/ipinfo.rb +4 -3
  14. data/lib/mihari/{web/entities → entities}/alert.rb +0 -0
  15. data/lib/mihari/{web/entities → entities}/artifact.rb +0 -0
  16. data/lib/mihari/{web/entities → entities}/autonomous_system.rb +0 -0
  17. data/lib/mihari/{web/entities → entities}/command.rb +0 -0
  18. data/lib/mihari/{web/entities → entities}/config.rb +0 -0
  19. data/lib/mihari/{web/entities → entities}/dns.rb +0 -0
  20. data/lib/mihari/{web/entities → entities}/geolocation.rb +0 -0
  21. data/lib/mihari/{web/entities → entities}/ip_address.rb +0 -0
  22. data/lib/mihari/{web/entities → entities}/message.rb +0 -0
  23. data/lib/mihari/{web/entities → entities}/reverse_dns.rb +0 -0
  24. data/lib/mihari/{web/entities → entities}/rule.rb +0 -0
  25. data/lib/mihari/{web/entities → entities}/source.rb +0 -0
  26. data/lib/mihari/{web/entities → entities}/tag.rb +0 -0
  27. data/lib/mihari/{web/entities → entities}/whois.rb +0 -0
  28. data/lib/mihari/errors.rb +2 -0
  29. data/lib/mihari/feed/reader.rb +11 -55
  30. data/lib/mihari/http.rb +94 -0
  31. data/lib/mihari/mixins/error_notification.rb +20 -0
  32. data/lib/mihari/mixins/retriable.rb +12 -2
  33. data/lib/mihari/mixins/rule.rb +1 -2
  34. data/lib/mihari/structs/ipinfo.rb +2 -3
  35. data/lib/mihari/structs/rule.rb +30 -0
  36. data/lib/mihari/structs/shodan.rb +9 -1
  37. data/lib/mihari/version.rb +1 -1
  38. data/lib/mihari/web/api.rb +0 -20
  39. data/lib/mihari/web/app.rb +2 -2
  40. data/lib/mihari/web/endpoints/rules.rb +3 -1
  41. data/lib/mihari/web/middleware/error_notification_adapter.rb +19 -0
  42. data/lib/mihari/web/public/index.html +1 -1
  43. data/lib/mihari/web/public/redoc-static.html +1881 -165
  44. data/lib/mihari/web/public/static/css/app.43138058.css +1 -0
  45. data/lib/mihari/web/public/static/css/chunk-vendors.3ed9b08e.css +7 -0
  46. data/lib/mihari/web/public/static/fonts/fa-brands-400.1fd0b4d7.ttf +0 -0
  47. data/lib/mihari/web/public/static/fonts/fa-brands-400.5d5236fb.woff2 +0 -0
  48. data/lib/mihari/web/public/static/fonts/fa-regular-400.64b3730e.woff2 +0 -0
  49. data/lib/mihari/web/public/static/fonts/fa-regular-400.95a8a8af.ttf +0 -0
  50. data/lib/mihari/web/public/static/fonts/fa-solid-900.6115ad71.woff2 +0 -0
  51. data/lib/mihari/web/public/static/fonts/fa-solid-900.f0203cfc.ttf +0 -0
  52. data/lib/mihari/web/public/static/fonts/fa-v4compatibility.e1023515.ttf +0 -0
  53. data/lib/mihari/web/public/static/js/app-legacy.46b666f0.js +2 -0
  54. data/lib/mihari/web/public/static/js/app-legacy.46b666f0.js.map +1 -0
  55. data/lib/mihari/web/public/static/js/app.4818aedd.js +2 -0
  56. data/lib/mihari/web/public/static/js/app.4818aedd.js.map +1 -0
  57. data/lib/mihari/web/public/static/js/chunk-vendors-legacy.c99e452e.js +17 -0
  58. data/lib/mihari/web/public/static/js/chunk-vendors-legacy.c99e452e.js.map +1 -0
  59. data/lib/mihari/web/public/static/js/chunk-vendors.15e84e22.js +23 -0
  60. data/lib/mihari/web/public/static/js/chunk-vendors.15e84e22.js.map +1 -0
  61. data/lib/mihari.rb +63 -15
  62. data/mihari.gemspec +3 -3
  63. data/sig/lib/mihari/emitters/slack.rbs +29 -1
  64. data/sig/lib/mihari/feed/reader.rbs +2 -2
  65. data/sig/lib/mihari/http.rbs +65 -0
  66. data/sig/lib/mihari/mixins/error_notification.rbs +12 -0
  67. data/sig/lib/mihari/structs/rule.rbs +6 -0
  68. data/sig/lib/mihari.rbs +4 -8
  69. metadata +68 -55
  70. data/lib/mihari/cli/mixins/utils.rb +0 -72
  71. data/lib/mihari/emitters/stdout.rb +0 -22
  72. data/lib/mihari/notifiers/base.rb +0 -24
  73. data/lib/mihari/notifiers/exception_notifier.rb +0 -126
  74. data/lib/mihari/notifiers/slack.rb +0 -63
  75. data/sig/lib/mihari/cli/mixins/utils.rbs +0 -50
  76. data/sig/lib/mihari/notifiers/base.rbs +0 -18
  77. data/sig/lib/mihari/notifiers/exception_notifier.rbs +0 -75
  78. data/sig/lib/mihari/notifiers/slack.rbs +0 -50
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c9d5f66d8c2bc30eeb644d6841662778e5457db184e21a35d9bc689cbf653d25
4
- data.tar.gz: 64edb36cf057854f4216ac51158dfa3e2b2876bb89a81c9441e38a4015c9bc78
3
+ metadata.gz: 853724da55a4225403595284e6437da9816d016d99e380d239507e1eecdf35cd
4
+ data.tar.gz: 35e5ef84d38c67eaa1670a5e6261aed5c629966921ca3b14fb2509c9e3993c45
5
5
  SHA512:
6
- metadata.gz: 54eb1b87c28c9af6a2e045ecc1c4bacb575353fe7a7eaa7f9c5ba2a4cea0a15b46342707b444b3a65f9c14317c6b19c58c8085feb653f6167a57c7f3231ce284
7
- data.tar.gz: b67ece0bb87d24ca83773a1c50b80560b46505a069f26a9c4c0ffe09105f2fe05ddafb4cfd5d0e1e7b1603ae5f1a11ea1d7b161971c37207c07869acb49e9b6c
6
+ metadata.gz: 50855245bf70579da57c26859d2ab491e5238f7200c1de50cc7f46cafcd94c57e411e0fdbffdd60ddb00df2c8783ed051c87be24963fe10c5459784f337bd8d3
7
+ data.tar.gz: 436e6c0f3ceacb5b72f4212e83832759320cce109310736d87f6d1bfb49ac61f32a25a4d44cf133d2d5794466dea72bd69244997ccbbc6525418bc9aef403d0f
@@ -42,7 +42,7 @@ jobs:
42
42
  ruby: [2.7, "3.0"]
43
43
 
44
44
  steps:
45
- - uses: actions/checkout@v2
45
+ - uses: actions/checkout@v3
46
46
 
47
47
  - name: Install dependencies
48
48
  run: |
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
  #
@@ -145,7 +145,7 @@ module Mihari
145
145
  instance = klass.new("dummy")
146
146
  unless instance.configured?
147
147
  klass_name = klass.to_s.split("::").last
148
- raise ArgumentError, "#{klass_name} is not configured correctly"
148
+ raise ConfigurationError, "#{klass_name} is not configured correctly"
149
149
  end
150
150
  end
151
151
  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
@@ -6,8 +6,11 @@ module Mihari
6
6
  include Mixins::Configurable
7
7
  include Mixins::Retriable
8
8
 
9
- def self.inherited(child)
10
- Mihari.emitters << child
9
+ class << self
10
+ def inherited(child)
11
+ super
12
+ Mihari.emitters << child
13
+ end
11
14
  end
12
15
 
13
16
  # @return [Boolean]
@@ -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
@@ -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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
data/lib/mihari/errors.rb CHANGED
@@ -14,4 +14,6 @@ module Mihari
14
14
  class FeedParseError < Error; end
15
15
 
16
16
  class RuleValidationError < Error; end
17
+
18
+ class ConfigurationError < Error; end
17
19
  end
@@ -18,29 +18,21 @@ module Mihari
18
18
  def read
19
19
  return read_file(uri.path) if uri.scheme == "file"
20
20
 
21
- return get if http_request_method == "GET"
21
+ res = nil
22
+ client = HTTP.new(uri, headers: http_request_headers, payload: http_request_payload, payload_type: http_request_payload_type)
22
23
 
23
- post
24
- end
25
-
26
- def get
27
- uri.query = Addressable::URI.form_encode(http_request_payload)
28
- get = Net::HTTP::Get.new(uri)
29
-
30
- request(get)
31
- end
24
+ res = client.get if http_request_method == "GET"
25
+ res = client.post if http_request_method == "POST"
32
26
 
33
- def post
34
- post = Net::HTTP::Post.new(uri)
27
+ return [] if res.nil?
35
28
 
36
- case http_request_payload_type
37
- when "application/json"
38
- post.body = JSON.generate(http_request_payload)
39
- when "application/x-www-form-urlencoded"
40
- post.set_form_data(http_request_payload)
29
+ body = res.body
30
+ content_type = res["Content-Type"].to_s
31
+ if content_type.include?("application/json")
32
+ convert_as_json(body)
33
+ else
34
+ convert_as_csv(body)
41
35
  end
42
-
43
- request(post)
44
36
  end
45
37
 
46
38
  #
@@ -70,42 +62,6 @@ module Mihari
70
62
  CSV.new(text_without_comments).to_a.reject(&:empty?)
71
63
  end
72
64
 
73
- def https_options
74
- return { use_ssl: true } if uri.scheme == "https"
75
-
76
- {}
77
- end
78
-
79
- #
80
- # Make a HTTP request
81
- #
82
- # @param [Net::HTTPRequest] req
83
- #
84
- # @return [Array<Hash>]
85
- #
86
- def request(req)
87
- Net::HTTP.start(uri.host, uri.port, https_options) do |http|
88
- # set headers
89
- http_request_headers.each do |k, v|
90
- req[k] = v
91
- end
92
-
93
- response = http.request(req)
94
-
95
- code = response.code.to_i
96
- raise HttpError, "Unsupported response code returned: #{code}" if code != 200
97
-
98
- body = response.body
99
-
100
- content_type = response["Content-Type"].to_s
101
- if content_type.include?("application/json")
102
- convert_as_json(body)
103
- else
104
- convert_as_csv(body)
105
- end
106
- end
107
- end
108
-
109
65
  #
110
66
  # Read & convert a file
111
67
  #
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ class HTTP
5
+ attr_reader :uri, :headers, :payload_type, :payload
6
+
7
+ def initialize(uri, headers: {}, payload_type: nil, payload: {})
8
+ @uri = Addressable::URI.parse(uri)
9
+ @headers = headers
10
+ @payload_type = payload_type
11
+ @payload = payload
12
+ end
13
+
14
+ #
15
+ # Make a GET request
16
+ #
17
+ # @return [Net::HTTPResponse]
18
+ #
19
+ def get
20
+ uri.query = Addressable::URI.form_encode(payload)
21
+ get = Net::HTTP::Get.new(uri)
22
+
23
+ request(get)
24
+ end
25
+
26
+ #
27
+ # Make a POST request
28
+ #
29
+ # @return [Net::HTTPResponse]
30
+ #
31
+ def post
32
+ post = Net::HTTP::Post.new(uri)
33
+
34
+ case payload_type
35
+ when "application/json"
36
+ headers["content-type"] = "application/json" unless headers.key?("content-type")
37
+ post.body = JSON.generate(payload)
38
+ when "application/x-www-form-urlencoded"
39
+ headers["content-type"] = "application/x-www-form-urlencoded" unless headers.key?("content-type")
40
+ post.set_form_data(payload)
41
+ end
42
+
43
+ request(post)
44
+ end
45
+
46
+ class << self
47
+ def get(uri, headers: {}, payload_type: nil, payload: {})
48
+ client = new(uri, headers: headers, payload_type: payload_type, payload: payload)
49
+ client.get
50
+ end
51
+
52
+ def post(uri, headers: {}, payload_type: nil, payload: {})
53
+ client = new(uri, headers: headers, payload_type: payload_type, payload: payload)
54
+ client.post
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ #
61
+ # Get options for HTTP request
62
+ #
63
+ # @return [Hahs]
64
+ #
65
+ def https_options
66
+ return { use_ssl: true } if uri.scheme == "https"
67
+
68
+ {}
69
+ end
70
+
71
+ #
72
+ # Make a HTTP request
73
+ #
74
+ # @param [Net::HTTPRequest] req
75
+ #
76
+ # @return [Net::HTTPResponse]
77
+ #
78
+ def request(req)
79
+ Net::HTTP.start(uri.host, uri.port, https_options) do |http|
80
+ # set headers
81
+ headers.each do |k, v|
82
+ req[k] = v
83
+ end
84
+
85
+ res = http.request(req)
86
+
87
+ code = res.code.to_i
88
+ raise HttpError, "Unsupported response code returned: #{code}" if code != 200
89
+
90
+ res
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Mixins
5
+ module ErrorNotification
6
+ #
7
+ # Send an exception notification if there is any error in a block
8
+ #
9
+ # @return [Nil]
10
+ #
11
+ def with_error_notification
12
+ yield
13
+ rescue StandardError => e
14
+ Mihari.logger.error e
15
+
16
+ Sentry.capture_exception(e) if Sentry.initialized?
17
+ end
18
+ end
19
+ end
20
+ end
@@ -3,20 +3,30 @@
3
3
  module Mihari
4
4
  module Mixins
5
5
  module Retriable
6
+ DEFAULT_ON = [
7
+ Errno::ECONNRESET,
8
+ Errno::ECONNABORTED,
9
+ Errno::EPIPE,
10
+ OpenSSL::SSL::SSLError,
11
+ Timeout::Error,
12
+ RetryableError
13
+ ]
14
+
6
15
  #
7
16
  # Retry on error
8
17
  #
9
18
  # @param [Integer] times
10
19
  # @param [Integer] interval
20
+ # @param [Array<StandardError>] on
11
21
  #
12
22
  # @return [nil]
13
23
  #
14
- def retry_on_error(times: 3, interval: 10)
24
+ def retry_on_error(times: 3, interval: 5, on: DEFAULT_ON)
15
25
  try = 0
16
26
  begin
17
27
  try += 1
18
28
  yield
19
- rescue Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE, OpenSSL::SSL::SSLError, Timeout::Error, RetryableError => e
29
+ rescue *on => e
20
30
  sleep interval
21
31
  retry if try < times
22
32
  raise e
@@ -48,8 +48,7 @@ module Mihari
48
48
  def validate_rule!(rule)
49
49
  rule.validate!
50
50
  rescue RuleValidationError => e
51
- error_message = "Failed to parse the input as a rule:\n#{e.message}"
52
- puts error_message.colorize(:red)
51
+ Mihari.logger.error "Failed to parse the input as a rule"
53
52
  raise e
54
53
  end
55
54
 
@@ -14,13 +14,12 @@ module Mihari
14
14
  include Mixins::AutonomousSystem
15
15
 
16
16
  def from_dynamic!(d)
17
+ d = d.deep_stringify_keys
17
18
  d = Types::Hash[d]
18
19
 
19
20
  asn = nil
20
21
  asn_ = d.dig("asn", "asn")
21
- unless asn_.nil?
22
- asn = normalize_asn(asn_)
23
- end
22
+ asn = normalize_asn(asn_) unless asn_.nil?
24
23
 
25
24
  new(
26
25
  ip: d.fetch("ip"),