mihari 5.4.3 → 5.4.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -25
  3. data/docs/alternatives.md +5 -0
  4. data/docs/analyzers/binaryedge.md +21 -0
  5. data/docs/analyzers/censys.md +23 -0
  6. data/docs/analyzers/circl.md +29 -0
  7. data/docs/analyzers/crtsh.md +25 -0
  8. data/docs/analyzers/dnstwister.md +23 -0
  9. data/docs/analyzers/feed.md +49 -0
  10. data/docs/analyzers/greynoise.md +21 -0
  11. data/docs/analyzers/hunterhow.md +25 -0
  12. data/docs/analyzers/index.md +79 -0
  13. data/docs/analyzers/onyphe.md +21 -0
  14. data/docs/analyzers/otx.md +23 -0
  15. data/docs/analyzers/passivetotal.md +36 -0
  16. data/docs/analyzers/pulsedive.md +23 -0
  17. data/docs/analyzers/securitytrails.md +32 -0
  18. data/docs/analyzers/shodan.md +21 -0
  19. data/docs/analyzers/urlscan.md +23 -0
  20. data/docs/analyzers/virustotal.md +34 -0
  21. data/docs/analyzers/virustotal_intelligence.md +22 -0
  22. data/docs/analyzers/zoomeye.md +25 -0
  23. data/docs/configuration.md +35 -0
  24. data/docs/emitters/database.md +22 -0
  25. data/docs/emitters/hive.md +18 -0
  26. data/docs/emitters/index.md +7 -0
  27. data/docs/emitters/misp.md +16 -0
  28. data/docs/emitters/slack.md +16 -0
  29. data/docs/emitters/webhook.md +63 -0
  30. data/docs/enrichers/google_public_dns.md +19 -0
  31. data/docs/enrichers/index.md +6 -0
  32. data/docs/enrichers/ipinfo.md +19 -0
  33. data/docs/enrichers/shodan.md +22 -0
  34. data/docs/enrichers/whois.md +17 -0
  35. data/docs/github_actions.md +43 -0
  36. data/docs/index.md +13 -0
  37. data/docs/installation.md +31 -0
  38. data/docs/requirements.md +20 -0
  39. data/docs/rule.md +165 -0
  40. data/docs/tags.md +3 -0
  41. data/docs/usage.md +100 -0
  42. data/frontend/package-lock.json +2414 -1516
  43. data/frontend/package.json +22 -22
  44. data/lib/mihari/analyzers/base.rb +25 -10
  45. data/lib/mihari/analyzers/binaryedge.rb +1 -7
  46. data/lib/mihari/analyzers/circl.rb +1 -1
  47. data/lib/mihari/analyzers/dnstwister.rb +1 -1
  48. data/lib/mihari/analyzers/otx.rb +1 -1
  49. data/lib/mihari/analyzers/passivetotal.rb +1 -1
  50. data/lib/mihari/analyzers/pulsedive.rb +1 -1
  51. data/lib/mihari/analyzers/rule.rb +18 -13
  52. data/lib/mihari/analyzers/securitytrails.rb +1 -1
  53. data/lib/mihari/analyzers/urlscan.rb +1 -1
  54. data/lib/mihari/analyzers/virustotal.rb +1 -1
  55. data/lib/mihari/analyzers/zoomeye.rb +1 -1
  56. data/lib/mihari/clients/binaryedge.rb +4 -7
  57. data/lib/mihari/clients/crtsh.rb +1 -3
  58. data/lib/mihari/clients/publsedive.rb +1 -1
  59. data/lib/mihari/clients/shodan.rb +2 -2
  60. data/lib/mihari/commands/alert.rb +42 -13
  61. data/lib/mihari/commands/rule.rb +11 -7
  62. data/lib/mihari/commands/search.rb +54 -22
  63. data/lib/mihari/config.rb +5 -0
  64. data/lib/mihari/emitters/base.rb +9 -3
  65. data/lib/mihari/emitters/slack.rb +1 -1
  66. data/lib/mihari/enrichers/base.rb +13 -0
  67. data/lib/mihari/enrichers/google_public_dns.rb +16 -1
  68. data/lib/mihari/enrichers/ipinfo.rb +9 -13
  69. data/lib/mihari/enrichers/shodan.rb +1 -2
  70. data/lib/mihari/enrichers/whois.rb +2 -2
  71. data/lib/mihari/errors.rb +16 -10
  72. data/lib/mihari/feed/parser.rb +2 -2
  73. data/lib/mihari/models/artifact.rb +1 -1
  74. data/lib/mihari/models/autonomous_system.rb +11 -5
  75. data/lib/mihari/models/cpe.rb +10 -4
  76. data/lib/mihari/models/dns.rb +11 -16
  77. data/lib/mihari/models/geolocation.rb +11 -5
  78. data/lib/mihari/models/port.rb +10 -4
  79. data/lib/mihari/models/reverse_dns.rb +10 -4
  80. data/lib/mihari/models/whois.rb +4 -1
  81. data/lib/mihari/schemas/analyzer.rb +1 -0
  82. data/lib/mihari/services/alert_builder.rb +43 -0
  83. data/lib/mihari/services/alert_proxy.rb +7 -25
  84. data/lib/mihari/services/alert_runner.rb +9 -0
  85. data/lib/mihari/services/rule_builder.rb +47 -0
  86. data/lib/mihari/services/rule_proxy.rb +5 -61
  87. data/lib/mihari/services/rule_runner.rb +9 -4
  88. data/lib/mihari/structs/binaryedge.rb +89 -0
  89. data/lib/mihari/structs/shodan.rb +2 -1
  90. data/lib/mihari/structs/urlscan.rb +1 -3
  91. data/lib/mihari/structs/virustotal_intelligence.rb +1 -3
  92. data/lib/mihari/type_checker.rb +1 -1
  93. data/lib/mihari/version.rb +1 -1
  94. data/lib/mihari/web/endpoints/alerts.rb +33 -15
  95. data/lib/mihari/web/endpoints/artifacts.rb +53 -25
  96. data/lib/mihari/web/endpoints/configs.rb +2 -2
  97. data/lib/mihari/web/endpoints/ip_addresses.rb +3 -5
  98. data/lib/mihari/web/endpoints/rules.rb +97 -71
  99. data/lib/mihari/web/endpoints/tags.rb +15 -5
  100. data/lib/mihari/web/public/assets/index-0a5a47bf.js +1740 -0
  101. data/lib/mihari/web/public/index.html +1 -1
  102. data/lib/mihari/web/public/redoc-static.html +419 -382
  103. data/lib/mihari.rb +4 -0
  104. data/mihari.gemspec +6 -5
  105. data/mkdocs.yml +35 -0
  106. data/requirements.txt +2 -0
  107. metadata +70 -12
  108. data/lib/mihari/web/public/assets/index-4d7eda9f.js +0 -1738
@@ -1,11 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "date"
4
- require "erb"
5
3
  require "json"
6
- require "pathname"
7
- require "securerandom"
8
- require "yaml"
9
4
 
10
5
  module Mihari
11
6
  module Services
@@ -29,10 +24,9 @@ module Mihari
29
24
  #
30
25
  def initialize(data)
31
26
  @data = data.deep_symbolize_keys
32
-
33
27
  @errors = nil
34
28
 
35
- validate
29
+ validate!
36
30
  end
37
31
 
38
32
  #
@@ -44,21 +38,14 @@ module Mihari
44
38
  !@errors.empty?
45
39
  end
46
40
 
47
- def validate
41
+ def validate!
48
42
  contract = Schemas::RuleContract.new
49
43
  result = contract.call(data)
50
44
 
51
45
  @data = result.to_h
52
46
  @errors = result.errors
53
- end
54
47
 
55
- def validate!
56
- return unless errors?
57
-
58
- Mihari.logger.error "Failed to parse the input as a rule:"
59
- Mihari.logger.error JSON.pretty_generate(errors.to_h)
60
-
61
- raise RuleValidationError, errors
48
+ raise ValidationError.new("Validation failed", errors) if errors?
62
49
  end
63
50
 
64
51
  def [](key)
@@ -178,9 +165,7 @@ module Mihari
178
165
  # @return [Mihari::Services::Rule]
179
166
  #
180
167
  def from_yaml(yaml)
181
- Services::RuleProxy.new YAML.safe_load(ERB.new(yaml).result, permitted_classes: [Date, Symbol])
182
- rescue Psych::SyntaxError => e
183
- raise YAMLSyntaxError, e.message
168
+ new YAML.safe_load(ERB.new(yaml).result, permitted_classes: [Date, Symbol])
184
169
  end
185
170
 
186
171
  #
@@ -189,48 +174,7 @@ module Mihari
189
174
  # @return [Mihari::Services::Rule]
190
175
  #
191
176
  def from_model(model)
192
- Services::RuleProxy.new model.data
193
- end
194
-
195
- #
196
- # Load a rule from path
197
- #
198
- # @param [String] path
199
- #
200
- # @return [Mihari::Services::Rule, nil]
201
- #
202
- def from_path(path)
203
- return nil unless Pathname(path).exist?
204
-
205
- from_yaml File.read(path)
206
- end
207
-
208
- #
209
- # Load a rule from DB
210
- #
211
- # @param [String] id
212
- #
213
- # @return [Mihari::Services::Rule, nil]
214
- #
215
- def from_id(id)
216
- return nil unless Mihari::Rule.exists?(id)
217
-
218
- Services::RuleProxy.from_model Mihari::Rule.find(id)
219
- end
220
-
221
- #
222
- # @param [String] path_or_id Path to YAML file or YAML string or ID of a rule in the database
223
- #
224
- # @return [Mihari::Services::Rule]
225
- #
226
- def from_path_or_id(path_or_id)
227
- rule = from_path(path_or_id)
228
- return rule unless rule.nil?
229
-
230
- rule = from_id(path_or_id)
231
- return rule unless rule.nil?
232
-
233
- raise ArgumentError, "#{path_or_id} does not exist"
177
+ new model.data
234
178
  end
235
179
  end
236
180
  end
@@ -3,6 +3,8 @@
3
3
  module Mihari
4
4
  module Services
5
5
  class RuleRunner
6
+ include Dry::Monads[:result, :try]
7
+
6
8
  include Mixins::ErrorNotification
7
9
 
8
10
  # @return [Mihari::Services::RuleProxy]
@@ -38,11 +40,14 @@ module Mihari
38
40
  # @return [Mihari::Alert, nil]
39
41
  #
40
42
  def run
41
- analyzer = rule.analyzer
43
+ rule.analyzer.run
44
+ end
42
45
 
43
- with_error_notification do
44
- analyzer.run
45
- end
46
+ #
47
+ # @return [Dry::Monads::Result::Success<Mihari::Alert, nil>, Dry::Monads::Result::Failure]
48
+ #
49
+ def result
50
+ Try[StandardError] { run }.to_result
46
51
  end
47
52
  end
48
53
  end
@@ -0,0 +1,89 @@
1
+ module Mihari
2
+ module Structs
3
+ module BinaryEdge
4
+ class Target < Dry::Struct
5
+ attribute :ip, Types::String
6
+
7
+ #
8
+ # @return [String]
9
+ #
10
+ def ip
11
+ attributes[:ip]
12
+ end
13
+
14
+ class << self
15
+ def from_dynamic!(d)
16
+ d = Types::Hash[d]
17
+ new(
18
+ ip: d.fetch("ip")
19
+ )
20
+ end
21
+ end
22
+ end
23
+
24
+ class Event < Dry::Struct
25
+ attribute :target, Target
26
+
27
+ #
28
+ # @return [Target]
29
+ #
30
+ def target
31
+ attributes[:target]
32
+ end
33
+
34
+ class << self
35
+ def from_dynamic!(d)
36
+ d = Types::Hash[d]
37
+ new(
38
+ target: Target.from_dynamic!(d.fetch("target"))
39
+ )
40
+ end
41
+ end
42
+ end
43
+
44
+ class Response < Dry::Struct
45
+ # @!attribute [r] page
46
+ # @return [Integer]
47
+ attribute :page, Types::Integer
48
+
49
+ # @!attribute [r] pagesize
50
+ # @return [Integer]
51
+ attribute :pagesize, Types::Integer
52
+
53
+ # @!attribute [r] total
54
+ # @return [Integer]
55
+ attribute :total, Types::Integer
56
+
57
+ # @!attribute [r] events
58
+ # @return [Array<Event>]
59
+ attribute :events, Types.Array(Event)
60
+
61
+ #
62
+ # @return [Array<Event>]
63
+ #
64
+ def events
65
+ attributes[:events]
66
+ end
67
+
68
+ #
69
+ # @return [Array<Artifact>]
70
+ #
71
+ def artifacts
72
+ events.map { |event| Artifact.new(data: event.target.ip) }
73
+ end
74
+
75
+ class << self
76
+ def from_dynamic!(d)
77
+ d = Types::Hash[d]
78
+ new(
79
+ page: d.fetch("page"),
80
+ pagesize: d.fetch("pagesize"),
81
+ total: d.fetch("total"),
82
+ events: d.fetch("events").map { |x| Event.from_dynamic!(x) }
83
+ )
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -140,7 +140,7 @@ module Mihari
140
140
  end
141
141
  end
142
142
 
143
- class Result < Dry::Struct
143
+ class Response < Dry::Struct
144
144
  attribute :matches, Types.Array(Match)
145
145
  attribute :total, Types::Int
146
146
 
@@ -197,6 +197,7 @@ module Mihari
197
197
  def artifacts
198
198
  matches.map do |match|
199
199
  metadata = collect_metadata_by_ip(match.ip_str)
200
+
200
201
  ports = collect_ports_by_ip(match.ip_str).map do |port|
201
202
  Mihari::Port.new(port: port)
202
203
  end
@@ -85,9 +85,7 @@ module Mihari
85
85
  #
86
86
  def artifacts
87
87
  values = [page.url, page.domain, page.ip].compact
88
- values.map do |value|
89
- Mihari::Artifact.new(data: value, metadata: metadata)
90
- end
88
+ values.map { |value| Mihari::Artifact.new(data: value, metadata: metadata) }
91
89
  end
92
90
 
93
91
  class << self
@@ -21,9 +21,7 @@ module Mihari
21
21
  #
22
22
  def from_dynamic!(d)
23
23
  d = Types::Hash[d]
24
- new(
25
- url: d["url"]
26
- )
24
+ new(url: d["url"])
27
25
  end
28
26
  end
29
27
  end
@@ -54,7 +54,7 @@ module Mihari
54
54
  return "ip" if ip?
55
55
  return "domain" if domain?
56
56
  return "url" if url?
57
- return "mail" if mail?
57
+ "mail" if mail?
58
58
  end
59
59
 
60
60
  # @return [String, nil]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mihari
4
- VERSION = "5.4.3"
4
+ VERSION = "5.4.5"
5
5
  end
@@ -54,18 +54,27 @@ module Mihari
54
54
  requires :id, type: Integer
55
55
  end
56
56
  delete "/:id" do
57
+ extend Dry::Monads[:result, :try]
58
+
57
59
  id = params["id"].to_i
58
60
 
59
- begin
61
+ result = Try do
60
62
  alert = Mihari::Alert.find(id)
61
- rescue ActiveRecord::RecordNotFound
62
- error!({ message: "ID:#{id} is not found" }, 404)
63
- end
63
+ alert.destroy
64
+ end.to_result
64
65
 
65
- alert.destroy
66
+ if result.success?
67
+ status 204
68
+ return present({ message: "" }, with: Entities::Message)
69
+ end
66
70
 
67
- status 204
68
- present({ message: "" }, with: Entities::Message)
71
+ failure = result.failure
72
+ case failure
73
+ when ActiveRecord::RecordNotFound
74
+ error!({ message: "ID:#{id} is not found" }, 404)
75
+ else
76
+ raise failure
77
+ end
69
78
  end
70
79
 
71
80
  desc "Create an alert", {
@@ -77,17 +86,26 @@ module Mihari
77
86
  requires :artifacts, type: Array, documentation: { type: String, is_array: true, param_type: "body" }
78
87
  end
79
88
  post "/" do
80
- proxy = Services::AlertProxy.new(params.to_snake_keys)
81
- runner = Services::AlertRunner.new(proxy)
89
+ extend Dry::Monads[:result, :try]
82
90
 
83
- begin
84
- alert = runner.run
85
- rescue ActiveRecord::RecordNotFound
86
- error!({ message: "Rule:#{params["ruleId"]} is not found" }, 404)
91
+ result = Try do
92
+ proxy = Services::AlertProxy.new(params.to_snake_keys)
93
+ runner = Services::AlertRunner.new(proxy)
94
+ runner.run
95
+ end.to_result
96
+
97
+ if result.success?
98
+ status 201
99
+ return present(result.value!, with: Entities::Alert)
87
100
  end
88
101
 
89
- status 201
90
- present alert, with: Entities::Alert
102
+ failure = result.failure
103
+ case failure
104
+ when ActiveRecord::RecordNotFound
105
+ error!({ message: "Rule:#{params["ruleId"]} is not found" }, 404)
106
+ else
107
+ raise failure
108
+ end
91
109
  end
92
110
  end
93
111
  end
@@ -13,9 +13,11 @@ module Mihari
13
13
  requires :id, type: Integer
14
14
  end
15
15
  get "/:id" do
16
+ extend Dry::Monads[:result, :try]
17
+
16
18
  id = params[:id].to_i
17
19
 
18
- begin
20
+ result = Try do
19
21
  artifact = Mihari::Artifact.includes(
20
22
  :autonomous_system,
21
23
  :geolocation,
@@ -23,18 +25,25 @@ module Mihari
23
25
  :dns_records,
24
26
  :reverse_dns_names
25
27
  ).find(id)
26
- rescue ActiveRecord::RecordNotFound
27
- error!({ message: "ID:#{id} is not found" }, 404)
28
- end
28
+ # TODO: improve queries
29
+ alert_ids = Mihari::Artifact.where(data: artifact.data).pluck(:alert_id)
30
+ tag_ids = Mihari::Tagging.where(alert_id: alert_ids).pluck(:tag_id)
31
+ tag_names = Mihari::Tag.where(id: tag_ids).distinct.pluck(:name)
29
32
 
30
- # TODO: improve queries
31
- alert_ids = Mihari::Artifact.where(data: artifact.data).pluck(:alert_id)
32
- tag_ids = Mihari::Tagging.where(alert_id: alert_ids).pluck(:tag_id)
33
- tag_names = Mihari::Tag.where(id: tag_ids).distinct.pluck(:name)
33
+ artifact.tags = tag_names
34
34
 
35
- artifact.tags = tag_names
35
+ artifact
36
+ end.to_result
36
37
 
37
- present artifact, with: Entities::Artifact
38
+ return present(result.value!, with: Entities::Artifact) if result.success?
39
+
40
+ failure = result.failure
41
+ case failure
42
+ when ActiveRecord::RecordNotFound
43
+ error!({ message: "ID:#{id} is not found" }, 404)
44
+ else
45
+ raise failure
46
+ end
38
47
  end
39
48
 
40
49
  desc "Enrich an artifact", {
@@ -46,9 +55,11 @@ module Mihari
46
55
  requires :id, type: Integer
47
56
  end
48
57
  get "/:id/enrich" do
58
+ extend Dry::Monads[:result, :try]
59
+
49
60
  id = params["id"].to_i
50
61
 
51
- begin
62
+ result = Try do
52
63
  artifact = Mihari::Artifact.includes(
53
64
  :autonomous_system,
54
65
  :geolocation,
@@ -58,15 +69,23 @@ module Mihari
58
69
  :cpes,
59
70
  :ports
60
71
  ).find(id)
61
- rescue ActiveRecord::RecordNotFound
62
- error!({ message: "ID:#{id} is not found" }, 404)
63
- end
64
72
 
65
- artifact.enrich_all
66
- artifact.save
73
+ artifact.enrich_all
74
+ artifact.save
75
+ end.to_result
67
76
 
68
- status 201
69
- present({ message: "" }, with: Entities::Message)
77
+ if result.success?
78
+ status 201
79
+ return present({ message: "" }, with: Entities::Message)
80
+ end
81
+
82
+ failure = result.failure
83
+ case failure
84
+ when ActiveRecord::RecordNotFound
85
+ error!({ message: "ID:#{id} is not found" }, 404)
86
+ else
87
+ raise failure
88
+ end
70
89
  end
71
90
 
72
91
  desc "Delete an artifact", {
@@ -78,18 +97,27 @@ module Mihari
78
97
  requires :id, type: Integer
79
98
  end
80
99
  delete "/:id" do
100
+ extend Dry::Monads[:result, :try]
101
+
81
102
  id = params["id"].to_i
82
103
 
83
- begin
104
+ result = Try do
84
105
  alert = Mihari::Artifact.find(id)
85
- rescue ActiveRecord::RecordNotFound
86
- error!({ message: "ID:#{id} is not found" }, 404)
87
- end
106
+ alert.destroy
107
+ end.to_result
88
108
 
89
- alert.destroy
109
+ if result.success?
110
+ status 204
111
+ return present({ message: "" }, with: Entities::Message)
112
+ end
90
113
 
91
- status 204
92
- present({ message: "" }, with: Entities::Message)
114
+ failure = result.failure
115
+ case failure
116
+ when ActiveRecord::RecordNotFound
117
+ error!({ message: "ID:#{id} is not found" }, 404)
118
+ else
119
+ raise failure
120
+ end
93
121
  end
94
122
  end
95
123
  end
@@ -10,9 +10,9 @@ module Mihari
10
10
  summary: "Get configs"
11
11
  }
12
12
  get "/" do
13
- configs = (Mihari.analyzers + Mihari.emitters + Mihari.enrichers).map do |klass|
13
+ configs = (Mihari.analyzers + Mihari.emitters + Mihari.enrichers).filter_map do |klass|
14
14
  Mihari::Structs::Config.from_class(klass)
15
- end.compact
15
+ end
16
16
 
17
17
  present(configs, with: Entities::Config)
18
18
  end
@@ -16,11 +16,9 @@ module Mihari
16
16
  ip = params[:ip].to_s
17
17
 
18
18
  data = Enrichers::IPInfo.query(ip)
19
- if data.nil?
20
- error!({ message: "IP:#{ip} is not found" }, 404)
21
- else
22
- present data, with: Entities::IPAddress
23
- end
19
+ error!({ message: "IP:#{ip} is not found" }, 404) if data.nil?
20
+
21
+ present data, with: Entities::IPAddress
24
22
  end
25
23
  end
26
24
  end