mihari 5.4.3 → 5.4.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/frontend/package-lock.json +2399 -1504
  3. data/frontend/package.json +22 -22
  4. data/lib/mihari/analyzers/base.rb +25 -10
  5. data/lib/mihari/analyzers/binaryedge.rb +1 -7
  6. data/lib/mihari/analyzers/circl.rb +1 -1
  7. data/lib/mihari/analyzers/dnstwister.rb +1 -1
  8. data/lib/mihari/analyzers/otx.rb +1 -1
  9. data/lib/mihari/analyzers/passivetotal.rb +1 -1
  10. data/lib/mihari/analyzers/pulsedive.rb +1 -1
  11. data/lib/mihari/analyzers/rule.rb +18 -13
  12. data/lib/mihari/analyzers/securitytrails.rb +1 -1
  13. data/lib/mihari/analyzers/urlscan.rb +1 -1
  14. data/lib/mihari/analyzers/virustotal.rb +1 -1
  15. data/lib/mihari/analyzers/zoomeye.rb +1 -1
  16. data/lib/mihari/clients/binaryedge.rb +4 -7
  17. data/lib/mihari/clients/crtsh.rb +1 -3
  18. data/lib/mihari/clients/publsedive.rb +1 -1
  19. data/lib/mihari/clients/shodan.rb +2 -2
  20. data/lib/mihari/commands/alert.rb +42 -13
  21. data/lib/mihari/commands/rule.rb +11 -7
  22. data/lib/mihari/commands/search.rb +54 -22
  23. data/lib/mihari/config.rb +5 -0
  24. data/lib/mihari/emitters/base.rb +9 -3
  25. data/lib/mihari/emitters/slack.rb +1 -1
  26. data/lib/mihari/enrichers/base.rb +13 -0
  27. data/lib/mihari/enrichers/google_public_dns.rb +16 -1
  28. data/lib/mihari/enrichers/ipinfo.rb +9 -13
  29. data/lib/mihari/enrichers/shodan.rb +1 -2
  30. data/lib/mihari/enrichers/whois.rb +2 -2
  31. data/lib/mihari/errors.rb +16 -10
  32. data/lib/mihari/feed/parser.rb +2 -2
  33. data/lib/mihari/models/artifact.rb +1 -1
  34. data/lib/mihari/models/autonomous_system.rb +11 -5
  35. data/lib/mihari/models/cpe.rb +10 -4
  36. data/lib/mihari/models/dns.rb +11 -16
  37. data/lib/mihari/models/geolocation.rb +11 -5
  38. data/lib/mihari/models/port.rb +10 -4
  39. data/lib/mihari/models/reverse_dns.rb +10 -4
  40. data/lib/mihari/models/whois.rb +4 -1
  41. data/lib/mihari/schemas/analyzer.rb +1 -0
  42. data/lib/mihari/services/alert_builder.rb +43 -0
  43. data/lib/mihari/services/alert_proxy.rb +7 -25
  44. data/lib/mihari/services/alert_runner.rb +9 -0
  45. data/lib/mihari/services/rule_builder.rb +47 -0
  46. data/lib/mihari/services/rule_proxy.rb +5 -61
  47. data/lib/mihari/services/rule_runner.rb +9 -4
  48. data/lib/mihari/structs/binaryedge.rb +89 -0
  49. data/lib/mihari/structs/shodan.rb +2 -1
  50. data/lib/mihari/structs/urlscan.rb +1 -3
  51. data/lib/mihari/structs/virustotal_intelligence.rb +1 -3
  52. data/lib/mihari/type_checker.rb +1 -1
  53. data/lib/mihari/version.rb +1 -1
  54. data/lib/mihari/web/endpoints/alerts.rb +33 -15
  55. data/lib/mihari/web/endpoints/artifacts.rb +53 -25
  56. data/lib/mihari/web/endpoints/configs.rb +2 -2
  57. data/lib/mihari/web/endpoints/ip_addresses.rb +3 -5
  58. data/lib/mihari/web/endpoints/rules.rb +97 -71
  59. data/lib/mihari/web/endpoints/tags.rb +15 -5
  60. data/lib/mihari/web/public/assets/index-ef33a6cd.js +1738 -0
  61. data/lib/mihari/web/public/index.html +1 -1
  62. data/lib/mihari/web/public/redoc-static.html +419 -382
  63. data/lib/mihari.rb +4 -0
  64. data/mihari.gemspec +5 -4
  65. metadata +28 -11
  66. data/lib/mihari/web/public/assets/index-4d7eda9f.js +0 -1738
@@ -3,6 +3,8 @@
3
3
  module Mihari
4
4
  module Emitters
5
5
  class Base
6
+ include Dry::Monads[:result, :try]
7
+
6
8
  include Mixins::Configurable
7
9
  include Mixins::Retriable
8
10
 
@@ -34,11 +36,15 @@ module Mihari
34
36
  raise NotImplementedError, "You must implement #{self.class}##{__method__}"
35
37
  end
36
38
 
37
- def run(**params)
38
- retry_on_error { emit(**params) }
39
+ def run
40
+ retry_on_error { emit }
41
+ end
42
+
43
+ def result
44
+ Try[StandardError] { run }.to_result
39
45
  end
40
46
 
41
- def emit(*)
47
+ def emit
42
48
  raise NotImplementedError, "You must implement #{self.class}##{__method__}"
43
49
  end
44
50
  end
@@ -114,7 +114,7 @@ module Mihari
114
114
 
115
115
  # @return [String]
116
116
  def defanged_data
117
- @defanged_data ||= data.to_s.gsub(/\./, "[.]")
117
+ @defanged_data ||= data.to_s.gsub(".", "[.]")
118
118
  end
119
119
  end
120
120
 
@@ -6,10 +6,23 @@ module Mihari
6
6
  include Mixins::Configurable
7
7
 
8
8
  class << self
9
+ include Dry::Monads[:result, :try]
10
+
9
11
  def inherited(child)
10
12
  super
11
13
  Mihari.enrichers << child
12
14
  end
15
+
16
+ def query_result(value)
17
+ Try[StandardError] { query(value) }.to_result
18
+ end
19
+
20
+ #
21
+ # @param [String] value
22
+ #
23
+ def query(value)
24
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
25
+ end
13
26
  end
14
27
 
15
28
  # @return [Boolean]
@@ -11,15 +11,30 @@ module Mihari
11
11
  end
12
12
 
13
13
  class << self
14
+ include Dry::Monads[:result]
15
+
14
16
  #
15
17
  # Query Google Public DNS
16
18
  #
17
19
  # @param [String] name
20
+ #
21
+ # @return [Array<Mihari::Structs::Shodan::GooglePublicDNS::Response>]
22
+ #
23
+ def query(name)
24
+ %w[A AAAA CNAME TXT NS].filter_map do |resource_type|
25
+ query_by_type(name, resource_type)
26
+ end
27
+ end
28
+
29
+ #
30
+ # Query Google Public DNS by resource type
31
+ #
32
+ # @param [String] name
18
33
  # @param [String] resource_type
19
34
  #
20
35
  # @return [Mihari::Structs::Shodan::GooglePublicDNS::Response, nil]
21
36
  #
22
- def query(name, resource_type)
37
+ def query_by_type(name, resource_type)
23
38
  url = "https://dns.google/resolve"
24
39
  params = { name: name, type: resource_type }
25
40
  res = HTTP.get(url, params: params)
@@ -17,6 +17,7 @@ module Mihari
17
17
  end
18
18
 
19
19
  class << self
20
+ include Dry::Monads[:result]
20
21
  include Memist::Memoizable
21
22
 
22
23
  #
@@ -28,20 +29,15 @@ module Mihari
28
29
  #
29
30
  def query(ip)
30
31
  headers = {}
32
+
31
33
  token = Mihari.config.ipinfo_api_key
32
- unless token.nil?
33
- headers[:authorization] = "Bearer #{token}"
34
- end
35
-
36
- begin
37
- url = "https://ipinfo.io/#{ip}/json"
38
- res = HTTP.get(url, headers: headers)
39
- data = JSON.parse(res.body.to_s)
40
-
41
- Structs::IPInfo::Response.from_dynamic! data
42
- rescue HTTPError
43
- nil
44
- end
34
+ headers[:authorization] = "Bearer #{token}" unless token.nil?
35
+
36
+ url = "https://ipinfo.io/#{ip}/json"
37
+ res = HTTP.get(url, headers: headers)
38
+ data = JSON.parse(res.body.to_s)
39
+
40
+ Structs::IPInfo::Response.from_dynamic! data
45
41
  end
46
42
  memoize :query
47
43
  end
@@ -11,6 +11,7 @@ module Mihari
11
11
  end
12
12
 
13
13
  class << self
14
+ include Dry::Monads[:result]
14
15
  include Memist::Memoizable
15
16
 
16
17
  #
@@ -26,8 +27,6 @@ module Mihari
26
27
  data = JSON.parse(res.body.to_s)
27
28
 
28
29
  Structs::Shodan::InternetDBResponse.from_dynamic! data
29
- rescue HTTPError
30
- nil
31
30
  end
32
31
  memoize :query
33
32
  end
@@ -14,6 +14,8 @@ module Mihari
14
14
  end
15
15
 
16
16
  class << self
17
+ include Dry::Monads[:result]
18
+
17
19
  #
18
20
  # Query IAIA Whois API
19
21
  #
@@ -47,8 +49,6 @@ module Mihari
47
49
  # set memo
48
50
  @memo[domain] = whois_record
49
51
  whois_record
50
- rescue ::Whois::Error, ::Whois::ParserError, Timeout::Error
51
- nil
52
52
  end
53
53
 
54
54
  def reset_cache
data/lib/mihari/errors.rb CHANGED
@@ -3,22 +3,14 @@
3
3
  module Mihari
4
4
  class Error < StandardError; end
5
5
 
6
- class InvalidInputError < Error; end
6
+ class ValueError < Error; end
7
7
 
8
- class InvalidArtifactFormatError < Error; end
8
+ class TypeError < Error; end
9
9
 
10
10
  class RetryableError < Error; end
11
11
 
12
12
  class FileNotFoundError < Error; end
13
13
 
14
- class FeedParseError < Error; end
15
-
16
- class RuleValidationError < Error; end
17
-
18
- class AlertValidationError < Error; end
19
-
20
- class YAMLSyntaxError < Error; end
21
-
22
14
  class ConfigurationError < Error; end
23
15
 
24
16
  # errors for HTTP interactions
@@ -49,4 +41,18 @@ module Mihari
49
41
  @body = body
50
42
  end
51
43
  end
44
+
45
+ class ValidationError < Error
46
+ attr_reader :errors
47
+
48
+ #
49
+ # @param [String] msg
50
+ # @param [Dry::Schema::MessageSet] errors
51
+ #
52
+ def initialize(msg, errors)
53
+ super(msg)
54
+
55
+ @errors = errors
56
+ end
57
+ end
52
58
  end
@@ -25,8 +25,8 @@ module Mihari
25
25
  def parse(selector)
26
26
  parsed = data.instance_eval(selector)
27
27
 
28
- raise FeedParseError unless parsed.is_a?(Array) || parsed.is_a?(Enumerator)
29
- raise FeedParseError unless parsed.all?(String)
28
+ raise TypeError unless parsed.is_a?(Array) || parsed.is_a?(Enumerator)
29
+ raise TypeError unless parsed.all?(String)
30
30
 
31
31
  parsed.to_a
32
32
  end
@@ -35,7 +35,7 @@ module Mihari
35
35
  attrs = args.first || kwargs
36
36
  data_ = attrs[:data]
37
37
 
38
- raise InvalidArtifactFormatError if data_.is_a?(Array) || data_.is_a?(Hash)
38
+ raise TypeError if data_.is_a?(Array) || data_.is_a?(Hash)
39
39
 
40
40
  super(*args, **kwargs)
41
41
 
@@ -5,6 +5,8 @@ module Mihari
5
5
  belongs_to :artifact
6
6
 
7
7
  class << self
8
+ include Dry::Monads[:result]
9
+
8
10
  #
9
11
  # Build AS
10
12
  #
@@ -13,11 +15,15 @@ module Mihari
13
15
  # @return [Mihari::AutonomousSystem, nil]
14
16
  #
15
17
  def build_by_ip(ip)
16
- res = Enrichers::IPInfo.query(ip)
17
-
18
- return nil if res.nil? || res.asn.nil?
19
-
20
- new(asn: res.asn)
18
+ result = Enrichers::IPInfo.query_result(ip).bind do |res|
19
+ value = res&.asn
20
+ if value.nil?
21
+ Success nil
22
+ else
23
+ Success new(asn: value)
24
+ end
25
+ end
26
+ result.value_or nil
21
27
  end
22
28
  end
23
29
  end
@@ -5,6 +5,8 @@ module Mihari
5
5
  belongs_to :artifact
6
6
 
7
7
  class << self
8
+ include Dry::Monads[:result]
9
+
8
10
  #
9
11
  # Build CPEs
10
12
  #
@@ -13,10 +15,14 @@ module Mihari
13
15
  # @return [Array<Mihari::CPE>]
14
16
  #
15
17
  def build_by_ip(ip)
16
- res = Enrichers::Shodan.query(ip)
17
- return [] if res.nil?
18
-
19
- res.cpes.map { |cpe| new(cpe: cpe) }
18
+ result = Enrichers::Shodan.query_result(ip).bind do |res|
19
+ if res.nil?
20
+ Success []
21
+ else
22
+ Success(res.cpes.map { |cpe| new(cpe: cpe) })
23
+ end
24
+ end
25
+ result.value_or []
20
26
  end
21
27
  end
22
28
  end
@@ -5,6 +5,8 @@ module Mihari
5
5
  belongs_to :artifact
6
6
 
7
7
  class << self
8
+ include Dry::Monads[:result]
9
+
8
10
  #
9
11
  # Build DNS records
10
12
  #
@@ -13,23 +15,16 @@ module Mihari
13
15
  # @return [Array<Mihari::DnsRecord>]
14
16
  #
15
17
  def build_by_domain(domain)
16
- resource_types = %w[A AAAA CNAME TXT NS]
17
- resource_types.map do |resource_type|
18
- get_values domain, resource_type
19
- rescue Resolv::ResolvError
20
- nil
21
- end.flatten.compact
22
- end
23
-
24
- private
25
-
26
- def get_values(domain, resource_type)
27
- response = Enrichers::GooglePublicDNS.query(domain, resource_type)
28
- answers = response.answers || []
29
-
30
- answers.filter_map do |answer|
31
- new(resource: answer.resource_type, value: answer.data)
18
+ result = Enrichers::GooglePublicDNS.query_result(domain).bind do |responses|
19
+ Success(
20
+ responses.map do |res|
21
+ res.answers.map do |answer|
22
+ new(resource: answer.resource_type, value: answer.data)
23
+ end
24
+ end.flatten
25
+ )
32
26
  end
27
+ result.value_or []
33
28
  end
34
29
  end
35
30
  end
@@ -7,6 +7,8 @@ module Mihari
7
7
  belongs_to :artifact
8
8
 
9
9
  class << self
10
+ include Dry::Monads[:result]
11
+
10
12
  #
11
13
  # Build Geolocation
12
14
  #
@@ -15,11 +17,15 @@ module Mihari
15
17
  # @return [Mihari::Geolocation, nil]
16
18
  #
17
19
  def build_by_ip(ip)
18
- res = Enrichers::IPInfo.query(ip)
19
-
20
- return nil if res&.country_code.nil?
21
-
22
- new(country: NormalizeCountry(res.country_code, to: :short), country_code: res.country_code)
20
+ result = Enrichers::IPInfo.query_result(ip).bind do |res|
21
+ value = res&.country_code
22
+ if value.nil?
23
+ Success nil
24
+ else
25
+ Success new(country: NormalizeCountry(value, to: :short), country_code: value)
26
+ end
27
+ end
28
+ result.value_or nil
23
29
  end
24
30
  end
25
31
  end
@@ -5,6 +5,8 @@ module Mihari
5
5
  belongs_to :artifact
6
6
 
7
7
  class << self
8
+ include Dry::Monads[:result]
9
+
8
10
  #
9
11
  # Build ports
10
12
  #
@@ -13,10 +15,14 @@ module Mihari
13
15
  # @return [Array<Mihari::Port>]
14
16
  #
15
17
  def build_by_ip(ip)
16
- res = Enrichers::Shodan.query(ip)
17
- return [] if res.nil?
18
-
19
- res.ports.map { |port| new(port: port) }
18
+ result = Enrichers::Shodan.query_result(ip).bind do |res|
19
+ if res.nil?
20
+ Success []
21
+ else
22
+ Success(res.ports.map { |port| new(port: port) })
23
+ end
24
+ end
25
+ result.value_or []
20
26
  end
21
27
  end
22
28
  end
@@ -5,6 +5,8 @@ module Mihari
5
5
  belongs_to :artifact
6
6
 
7
7
  class << self
8
+ include Dry::Monads[:result]
9
+
8
10
  #
9
11
  # Build reverse DNS names
10
12
  #
@@ -13,10 +15,14 @@ module Mihari
13
15
  # @return [Array<Mihari::ReverseDnsName>]
14
16
  #
15
17
  def build_by_ip(ip)
16
- res = Enrichers::Shodan.query(ip)
17
- return [] if res.nil?
18
-
19
- res.hostnames.map { |name| new(name: name) }
18
+ result = Enrichers::Shodan.query_result(ip).bind do |res|
19
+ if res.nil?
20
+ Success []
21
+ else
22
+ Success(res.hostnames.map { |name| new(name: name) })
23
+ end
24
+ end
25
+ result.value_or []
20
26
  end
21
27
  end
22
28
  end
@@ -7,6 +7,8 @@ module Mihari
7
7
  @memo = {}
8
8
 
9
9
  class << self
10
+ include Dry::Monads[:result]
11
+
10
12
  #
11
13
  # Build whois record
12
14
  #
@@ -15,7 +17,8 @@ module Mihari
15
17
  # @return [WhoisRecord, nil]
16
18
  #
17
19
  def build_by_domain(domain)
18
- Enrichers::Whois.query domain
20
+ result = Enrichers::Whois.query_result(domain)
21
+ result.value_or nil
19
22
  end
20
23
  end
21
24
  end
@@ -7,6 +7,7 @@ module Mihari
7
7
  optional(:pagination_limit).value(:integer).default(Mihari.config.pagination_limit)
8
8
  optional(:retry_times).value(:integer).default(Mihari.config.retry_times)
9
9
  optional(:retry_interval).value(:integer).default(Mihari.config.retry_interval)
10
+ optional(:ignore_error).value(:bool).default(Mihari.config.ignore_error)
10
11
  end
11
12
 
12
13
  AnalyzerWithoutAPIKey = Dry::Schema.Params do
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ require "erb"
6
+ require "pathname"
7
+ require "yaml"
8
+
9
+ module Mihari
10
+ module Services
11
+ class AlertBuilder
12
+ include Dry::Monads[:result, :try]
13
+
14
+ # @return [String]
15
+ attr_reader :path
16
+
17
+ #
18
+ # Initialize
19
+ #
20
+ # @param [String] path
21
+ #
22
+ def initialize(path)
23
+ @path = path
24
+ end
25
+
26
+ #
27
+ # @return [Hash]
28
+ #
29
+ def data
30
+ raise ArgumentError, "#{path} does not exist" unless Pathname(path).exist?
31
+
32
+ YAML.safe_load(
33
+ ERB.new(File.read(path)).result,
34
+ permitted_classes: [Date, Symbol]
35
+ )
36
+ end
37
+
38
+ def result
39
+ Try[StandardError] { AlertProxy.new(data) }.to_result
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+
3
5
  module Mihari
4
6
  module Services
5
7
  class AlertProxy
@@ -16,10 +18,9 @@ module Mihari
16
18
  #
17
19
  def initialize(data)
18
20
  @data = data.deep_symbolize_keys
19
-
20
21
  @errors = nil
21
22
 
22
- validate
23
+ validate!
23
24
  end
24
25
 
25
26
  #
@@ -31,21 +32,14 @@ module Mihari
31
32
  !@errors.empty?
32
33
  end
33
34
 
34
- def validate
35
+ def validate!
35
36
  contract = Schemas::AlertContract.new
36
37
  result = contract.call(data)
37
38
 
38
39
  @data = result.to_h
39
40
  @errors = result.errors
40
- end
41
-
42
- def validate!
43
- return unless errors?
44
41
 
45
- Mihari.logger.error "Failed to parse the input as an alert:"
46
- Mihari.logger.error JSON.pretty_generate(errors.to_h)
47
-
48
- raise AlertValidationError, errors
42
+ raise ValidationError.new("Validation failed", errors) if errors?
49
43
  end
50
44
 
51
45
  def [](key)
@@ -74,7 +68,7 @@ module Mihari
74
68
  # @return [Mihari::Services::RuleProxy]
75
69
  #
76
70
  def rule
77
- @rule ||= Services::RuleProxy.from_model(Mihari::Rule.find(rule_id))
71
+ @rule ||= Services::RuleProxy.new(Mihari::Rule.find(rule_id).data)
78
72
  end
79
73
 
80
74
  class << self
@@ -86,19 +80,7 @@ module Mihari
86
80
  # @return [Mihari::Services::Alert]
87
81
  #
88
82
  def from_yaml(yaml)
89
- Services::AlertProxy.new YAML.safe_load(yaml, permitted_classes: [Date, Symbol])
90
- rescue Psych::SyntaxError => e
91
- raise YAMLSyntaxError, e.message
92
- end
93
-
94
- # @param [String] path
95
- #
96
- # @return [Mihari::Services::Alert, nil]
97
- #
98
- def from_path(path)
99
- return nil unless Pathname(path).exist?
100
-
101
- from_yaml File.read(path)
83
+ new YAML.safe_load(yaml, permitted_classes: [Date, Symbol])
102
84
  end
103
85
  end
104
86
  end
@@ -3,6 +3,8 @@
3
3
  module Mihari
4
4
  module Services
5
5
  class AlertRunner
6
+ include Dry::Monads[:result, :try]
7
+
6
8
  # @return [Mihari::Services::AlertProxy]
7
9
  attr_reader :alert
8
10
 
@@ -17,6 +19,13 @@ module Mihari
17
19
  emitter = Mihari::Emitters::Database.new(artifacts: alert.artifacts, rule: alert.rule)
18
20
  emitter.emit
19
21
  end
22
+
23
+ #
24
+ # @return [Dry::Monads::Result::Success<Mihari::Alert, nil>, Dry::Monads::Result::Failure]
25
+ #
26
+ def result
27
+ Try[StandardError] { run }.to_result
28
+ end
20
29
  end
21
30
  end
22
31
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "erb"
5
+ require "pathname"
6
+ require "yaml"
7
+
8
+ module Mihari
9
+ module Services
10
+ class RuleBuilder
11
+ include Dry::Monads[:result, :try]
12
+
13
+ # @return [String]
14
+ attr_reader :path_or_id
15
+
16
+ #
17
+ # Initialize
18
+ #
19
+ # @param [String] path_or_id
20
+ #
21
+ def initialize(path_or_id)
22
+ @path_or_id = path_or_id
23
+ end
24
+
25
+ #
26
+ # @return [Hash]
27
+ #
28
+ def data
29
+ if Mihari::Rule.exists?(path_or_id)
30
+ rule = Mihari::Rule.find(path_or_id)
31
+ return rule.data
32
+ end
33
+
34
+ raise ArgumentError, "#{path_or_id} does not exist" unless Pathname(path_or_id).exist?
35
+
36
+ YAML.safe_load(
37
+ ERB.new(File.read(path_or_id)).result,
38
+ permitted_classes: [Date, Symbol]
39
+ )
40
+ end
41
+
42
+ def result
43
+ Try[StandardError] { RuleProxy.new(data) }.to_result
44
+ end
45
+ end
46
+ end
47
+ end