mihari 4.7.0 → 4.7.3

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