mihari 4.7.0 → 4.7.3

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mihari/analyzers/clients/otx.rb +36 -0
  3. data/lib/mihari/analyzers/otx.rb +19 -11
  4. data/lib/mihari/analyzers/rule.rb +0 -1
  5. data/lib/mihari/commands/init.rb +25 -2
  6. data/lib/mihari/commands/search.rb +2 -7
  7. data/lib/mihari/commands/validator.rb +10 -5
  8. data/lib/mihari/errors.rb +2 -0
  9. data/lib/mihari/models/alert.rb +6 -1
  10. data/lib/mihari/models/geolocation.rb +2 -4
  11. data/lib/mihari/models/port.rb +1 -1
  12. data/lib/mihari/models/rule.rb +7 -2
  13. data/lib/mihari/structs/filters.rb +71 -0
  14. data/lib/mihari/structs/ipinfo.rb +4 -4
  15. data/lib/mihari/structs/rule.rb +188 -139
  16. data/lib/mihari/version.rb +1 -1
  17. data/lib/mihari/web/endpoints/alerts.rb +1 -1
  18. data/lib/mihari/web/endpoints/rules.rb +13 -5
  19. data/lib/mihari/web/public/index.html +1 -1
  20. data/lib/mihari/web/public/redoc-static.html +796 -763
  21. data/lib/mihari/web/public/static/css/chunk-vendors.5013d549.css +7 -0
  22. data/lib/mihari/web/public/static/js/app.524d9ed2.js +2 -0
  23. data/lib/mihari/web/public/static/js/app.524d9ed2.js.map +1 -0
  24. data/lib/mihari/web/public/static/js/{chunk-vendors.dde2116c.js → chunk-vendors.64580a1f.js} +7 -7
  25. data/lib/mihari/web/public/static/js/chunk-vendors.64580a1f.js.map +1 -0
  26. data/lib/mihari.rb +1 -2
  27. data/mihari.gemspec +10 -11
  28. data/sig/lib/mihari/cli/base.rbs +0 -2
  29. data/sig/lib/mihari/models/alert.rbs +3 -3
  30. data/sig/lib/mihari/models/rule.rbs +2 -2
  31. data/sig/lib/mihari/structs/filters.rbs +40 -0
  32. data/sig/lib/mihari/structs/ipinfo.rbs +2 -2
  33. data/sig/lib/mihari/structs/rule.rbs +36 -43
  34. metadata +30 -49
  35. data/lib/mihari/mixins/rule.rb +0 -84
  36. data/lib/mihari/structs/alert.rb +0 -44
  37. data/lib/mihari/web/public/static/css/chunk-vendors.06251949.css +0 -7
  38. data/lib/mihari/web/public/static/js/app-legacy.9d5c9c3d.js +0 -2
  39. data/lib/mihari/web/public/static/js/app-legacy.9d5c9c3d.js.map +0 -1
  40. data/lib/mihari/web/public/static/js/app.823b5af7.js +0 -2
  41. data/lib/mihari/web/public/static/js/app.823b5af7.js.map +0 -1
  42. data/lib/mihari/web/public/static/js/chunk-vendors-legacy.b110c129.js +0 -25
  43. data/lib/mihari/web/public/static/js/chunk-vendors-legacy.b110c129.js.map +0 -1
  44. data/lib/mihari/web/public/static/js/chunk-vendors.dde2116c.js.map +0 -1
  45. data/sig/lib/mihari/mixins/rule.rbs +0 -36
  46. data/sig/lib/mihari/structs/alert.rbs +0 -27
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '091892853042ab3f1010c89b943eb2a885370da2d619016f3f3cd9dcb59e80cf'
4
- data.tar.gz: 119ccbb407a49c741a4bf6c7cfafc7cd99855c3af950af9d0f1dc3735fc24672
3
+ metadata.gz: c96da785a17ea290f65f5ce17b43d7fa4a57086d8de7ee867e8369f832f7c25c
4
+ data.tar.gz: 7b5c2d5e14d4b1a6c6c4434387881d34d3b04c33802ad84a4430d360b4d22e6c
5
5
  SHA512:
6
- metadata.gz: 5364837634a6cde1b370db5613e745f05b40bf7321a5b7fcc4a2c7d32b1d395fa45e4472ef2a5f9f28d664f0b45e31d0554acec7bb641a21ce179ebf70bf9317
7
- data.tar.gz: 57ca2f920db2da9e8c9a10f59aa81984dff0a81f0073ba839c351f3fe5e7979358ce44d87f48adcffd7df1bf7268bab178fec82731f72d62c24aa9ce04eed026
6
+ metadata.gz: f7d66fcac98d91d6910bcabc9a7a2b29f96e562619bcf0615e2f10e2bd25c72f5439f390836d27f083f5b8da4f6696a23358881a4d6b78a6e1ae5e5b57aaa70e
7
+ data.tar.gz: 4476349d708f35c064af3c4a1d126790fbb008af34a62e2a485d1f0ef72a097723233e046985cc4f88f6543f84caba480dd85f1076c3ba0638951fdcae07e8dd
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Analyzers
5
+ module Clients
6
+ class OTX
7
+ attr_reader :api_key
8
+
9
+ def initialize(api_key)
10
+ @api_key = api_key
11
+ end
12
+
13
+ def query_by_ip(ip)
14
+ get "https://otx.alienvault.com/api/v1/indicators/IPv4/#{ip}/passive_dns"
15
+ end
16
+
17
+ def query_by_domain(domain)
18
+ get "https://otx.alienvault.com/api/v1/indicators/domain/#{domain}/passive_dns"
19
+ end
20
+
21
+ private
22
+
23
+ def headers
24
+ { "x-otx-api-key": api_key }
25
+ end
26
+
27
+ def get(url)
28
+ res = HTTP.get(url, headers: headers)
29
+ JSON.parse(res.body.to_s)
30
+ rescue HTTPError
31
+ nil
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "otx_ruby"
3
+ require "mihari/analyzers/clients/otx"
4
4
 
5
5
  module Mihari
6
6
  module Analyzers
@@ -34,12 +34,8 @@ module Mihari
34
34
  %w[otx_api_key]
35
35
  end
36
36
 
37
- def domain_client
38
- @domain_client ||= ::OTX::Domain.new(api_key)
39
- end
40
-
41
- def ip_client
42
- @ip_client ||= ::OTX::IP.new(api_key)
37
+ def client
38
+ @client ||= Mihari::Analyzers::Clients::OTX.new(api_key)
43
39
  end
44
40
 
45
41
  #
@@ -73,9 +69,15 @@ module Mihari
73
69
  # @return [Array<String>]
74
70
  #
75
71
  def domain_search
76
- records = domain_client.get_passive_dns(query)
72
+ res = client.query_by_domain(query)
73
+ return [] if res.nil?
74
+
75
+ records = res["passive_dns"] || []
77
76
  records.filter_map do |record|
78
- record.address if record.record_type == "A"
77
+ record_type = record["record_type"]
78
+ address = record["address"]
79
+
80
+ address if record_type == "A"
79
81
  end.uniq
80
82
  end
81
83
 
@@ -85,9 +87,15 @@ module Mihari
85
87
  # @return [Array<String>]
86
88
  #
87
89
  def ip_search
88
- records = ip_client.get_passive_dns(query)
90
+ res = client.query_by_ip(query)
91
+ return [] if res.nil?
92
+
93
+ records = res["passive_dns"] || []
89
94
  records.filter_map do |record|
90
- record.hostname if record.record_type == "A"
95
+ record_type = record["record_type"]
96
+ hostname = record["hostname"]
97
+
98
+ hostname if record_type == "A"
91
99
  end.uniq
92
100
  end
93
101
  end
@@ -39,7 +39,6 @@ module Mihari
39
39
 
40
40
  class Rule < Base
41
41
  include Mixins::DisallowedDataValue
42
- include Mixins::Rule
43
42
 
44
43
  option :title
45
44
  option :description
@@ -3,8 +3,6 @@
3
3
  module Mihari
4
4
  module Commands
5
5
  module Initialization
6
- include Mixins::Rule
7
-
8
6
  def self.included(thor)
9
7
  thor.class_eval do
10
8
  desc "rule", "Create a rule file"
@@ -21,6 +19,31 @@ module Mihari
21
19
 
22
20
  Mihari.logger.info "The rule file is initialized as #{filename}."
23
21
  end
22
+
23
+ no_commands do
24
+ #
25
+ # Returns a template for rule
26
+ #
27
+ # @return [String] A template for rule
28
+ #
29
+ def rule_template
30
+ rule = Structs::Rule.from_path_or_id File.expand_path("../templates/rule.yml.erb", __dir__)
31
+ rule.yaml
32
+ end
33
+
34
+ #
35
+ # Create (blank) rule file
36
+ #
37
+ # @param [String] filename
38
+ # @param [Dry::Files] files
39
+ # @param [String] template
40
+ #
41
+ # @return [nil]
42
+ #
43
+ def initialize_rule_yaml(filename, files = Dry::Files.new, template: rule_template)
44
+ files.write(filename, template)
45
+ end
46
+ end
24
47
  end
25
48
  end
26
49
  end
@@ -4,7 +4,6 @@ module Mihari
4
4
  module Commands
5
5
  module Search
6
6
  include Mixins::Database
7
- include Mixins::Rule
8
7
  include Mixins::ErrorNotification
9
8
 
10
9
  def self.included(thor)
@@ -12,14 +11,10 @@ module Mihari
12
11
  desc "search [RULE]", "Search by a rule"
13
12
  method_option :yes, type: :boolean, aliases: "-y", desc: "yes to overwrite the rule in the database"
14
13
  def search_by_rule(path_or_id)
15
- rule = load_rule(path_or_id)
14
+ rule = Structs::Rule.from_path_or_id path_or_id
16
15
 
17
16
  # validate
18
- begin
19
- validate_rule! rule
20
- rescue RuleValidationError => e
21
- raise e
22
- end
17
+ rule.validate!
23
18
 
24
19
  # check update
25
20
  id = rule.id
@@ -3,16 +3,21 @@
3
3
  module Mihari
4
4
  module Commands
5
5
  module Validator
6
- include Mixins::Rule
7
-
8
6
  def self.included(thor)
9
7
  thor.class_eval do
10
- desc "rule [PATH]", "Validate format of a rule file"
8
+ desc "rule [PATH]", "Validate rule file format"
9
+ #
10
+ # Validate format of a rule
11
+ #
12
+ # @param [String] path
13
+ #
14
+ # @return [nil]
15
+ #
11
16
  def rule(path)
12
- rule = load_rule(path)
17
+ rule = Structs::Rule.from_path_or_id(path)
13
18
 
14
19
  begin
15
- validate_rule! rule
20
+ rule.validate!
16
21
  Mihari.logger.info "Valid format. The input is parsed as the following:\n#{rule.data.to_yaml}"
17
22
  rescue RuleValidationError
18
23
  nil
data/lib/mihari/errors.rb CHANGED
@@ -15,6 +15,8 @@ module Mihari
15
15
 
16
16
  class RuleValidationError < Error; end
17
17
 
18
+ class YAMLSyntaxError < Error; end
19
+
18
20
  class ConfigurationError < Error; end
19
21
 
20
22
  class HTTPError < Error; end
@@ -12,7 +12,7 @@ module Mihari
12
12
  #
13
13
  # Search alerts
14
14
  #
15
- # @param [Structs::Alert::SearchFilterWithPagination] filter
15
+ # @param [Structs::Filters::Alert::SearchFilterWithPagination] filter
16
16
  #
17
17
  # @return [Array<Alert>]
18
18
  #
@@ -58,6 +58,11 @@ module Mihari
58
58
 
59
59
  private
60
60
 
61
+ #
62
+ # @param [Structs::Filters::Alert::SearchFilter] filter
63
+ #
64
+ # @return [Mihari::Alert]
65
+ #
61
66
  def build_relation(filter)
62
67
  artifact_ids = []
63
68
  artifact = Artifact.includes(:autonomous_system, :dns_records, :reverse_dns_names)
@@ -17,11 +17,9 @@ module Mihari
17
17
  def build_by_ip(ip)
18
18
  res = Enrichers::IPInfo.query(ip)
19
19
 
20
- unless res.nil?
21
- return new(country: NormalizeCountry(res.country_code, to: :short), country_code: res.country_code)
22
- end
20
+ return nil if res&.country_code.nil?
23
21
 
24
- nil
22
+ new(country: NormalizeCountry(res.country_code, to: :short), country_code: res.country_code)
25
23
  end
26
24
  end
27
25
  end
@@ -14,7 +14,7 @@ module Mihari
14
14
  #
15
15
  def build_by_ip(ip)
16
16
  res = Enrichers::Shodan.query(ip)
17
- return if res.nil?
17
+ return [] if res.nil?
18
18
 
19
19
  res.ports.map { |port| new(port: port) }
20
20
  end
@@ -28,7 +28,7 @@ module Mihari
28
28
  #
29
29
  # Search rules
30
30
  #
31
- # @param [Structs::Rule::SearchFilterWithPagination] filter
31
+ # @param [Structs::Filters::Rule::SearchFilterWithPagination] filter
32
32
  #
33
33
  # @return [Array<Rule>]
34
34
  #
@@ -51,7 +51,7 @@ module Mihari
51
51
  #
52
52
  # Count alerts
53
53
  #
54
- # @param [Structs::Rule::SearchFilterWithPagination] filter
54
+ # @param [Structs::Filters::Rule::SearchFilterWithPagination] filter
55
55
  #
56
56
  # @return [Integer]
57
57
  #
@@ -62,6 +62,11 @@ module Mihari
62
62
 
63
63
  private
64
64
 
65
+ #
66
+ # @param [Structs::Filters::Rule::SearchFilter] filter
67
+ #
68
+ # @return [Mihari::Rule]
69
+ #
65
70
  def build_relation(filter)
66
71
  relation = self
67
72
  relation = relation.includes(alerts: :tags)
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Structs
5
+ module Filters
6
+ module Alert
7
+ class SearchFilter < Dry::Struct
8
+ attribute? :artifact_data, Types::String.optional
9
+ attribute? :description, Types::String.optional
10
+ attribute? :source, Types::String.optional
11
+ attribute? :tag_name, Types::String.optional
12
+ attribute? :title, Types::String.optional
13
+ attribute? :from_at, Types::DateTime.optional
14
+ attribute? :to_at, Types::DateTime.optional
15
+ attribute? :asn, Types::Int.optional
16
+ attribute? :dns_record, Types::String.optional
17
+ attribute? :reverse_dns_name, Types::String.optional
18
+
19
+ def valid_artifact_filters?
20
+ !(artifact_data || asn || dns_record || reverse_dns_name).nil?
21
+ end
22
+ end
23
+
24
+ class SearchFilterWithPagination < SearchFilter
25
+ attribute? :page, Types::Int.default(1)
26
+ attribute? :limit, Types::Int.default(10)
27
+
28
+ def without_pagination
29
+ SearchFilter.new(
30
+ artifact_data: artifact_data,
31
+ description: description,
32
+ from_at: from_at,
33
+ source: source,
34
+ tag_name: tag_name,
35
+ title: title,
36
+ to_at: to_at,
37
+ asn: asn,
38
+ dns_record: dns_record,
39
+ reverse_dns_name: reverse_dns_name
40
+ )
41
+ end
42
+ end
43
+ end
44
+
45
+ module Rule
46
+ class SearchFilter < Dry::Struct
47
+ attribute? :description, Types::String.optional
48
+ attribute? :tag_name, Types::String.optional
49
+ attribute? :title, Types::String.optional
50
+ attribute? :from_at, Types::DateTime.optional
51
+ attribute? :to_at, Types::DateTime.optional
52
+ end
53
+
54
+ class SearchFilterWithPagination < SearchFilter
55
+ attribute? :page, Types::Int.default(1)
56
+ attribute? :limit, Types::Int.default(10)
57
+
58
+ def without_pagination
59
+ SearchFilter.new(
60
+ description: description,
61
+ from_at: from_at,
62
+ tag_name: tag_name,
63
+ title: title,
64
+ to_at: to_at
65
+ )
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -6,8 +6,8 @@ module Mihari
6
6
  class Response < Dry::Struct
7
7
  attribute :ip, Types::String
8
8
  attribute :hostname, Types::String.optional
9
- attribute :loc, Types::String
10
- attribute :country_code, Types::String
9
+ attribute :loc, Types::String.optional
10
+ attribute :country_code, Types::String.optional
11
11
  attribute :asn, Types::Integer.optional
12
12
 
13
13
  class << self
@@ -23,9 +23,9 @@ module Mihari
23
23
 
24
24
  new(
25
25
  ip: d.fetch("ip"),
26
- loc: d.fetch("loc"),
26
+ loc: d["loc"],
27
27
  hostname: d["hostname"],
28
- country_code: d.fetch("country"),
28
+ country_code: d["country"],
29
29
  asn: asn
30
30
  )
31
31
  end