mihari 3.3.0 → 3.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -0
  3. data/config.ru +1 -0
  4. data/lib/mihari/analyzers/base.rb +34 -6
  5. data/lib/mihari/analyzers/censys.rb +37 -9
  6. data/lib/mihari/analyzers/onyphe.rb +34 -9
  7. data/lib/mihari/analyzers/shodan.rb +26 -5
  8. data/lib/mihari/cli/analyzer.rb +4 -0
  9. data/lib/mihari/cli/base.rb +0 -5
  10. data/lib/mihari/commands/init.rb +4 -4
  11. data/lib/mihari/commands/search.rb +17 -8
  12. data/lib/mihari/commands/web.rb +1 -0
  13. data/lib/mihari/{constraints.rb → constants.rb} +0 -0
  14. data/lib/mihari/database.rb +42 -3
  15. data/lib/mihari/mixins/rule.rb +5 -1
  16. data/lib/mihari/models/alert.rb +28 -10
  17. data/lib/mihari/models/artifact.rb +55 -0
  18. data/lib/mihari/models/autonomous_system.rb +9 -0
  19. data/lib/mihari/models/dns.rb +53 -0
  20. data/lib/mihari/models/geolocation.rb +9 -0
  21. data/lib/mihari/models/reverse_dns.rb +24 -0
  22. data/lib/mihari/models/whois.rb +119 -0
  23. data/lib/mihari/schemas/configuration.rb +1 -0
  24. data/lib/mihari/schemas/rule.rb +5 -15
  25. data/lib/mihari/serializers/alert.rb +6 -4
  26. data/lib/mihari/serializers/artifact.rb +11 -2
  27. data/lib/mihari/serializers/autonomous_system.rb +9 -0
  28. data/lib/mihari/serializers/dns.rb +11 -0
  29. data/lib/mihari/serializers/geolocation.rb +11 -0
  30. data/lib/mihari/serializers/reverse_dns.rb +11 -0
  31. data/lib/mihari/serializers/tag.rb +4 -2
  32. data/lib/mihari/serializers/whois.rb +11 -0
  33. data/lib/mihari/structs/censys.rb +92 -0
  34. data/lib/mihari/structs/onyphe.rb +47 -0
  35. data/lib/mihari/structs/shodan.rb +53 -0
  36. data/lib/mihari/templates/rule.yml.erb +3 -0
  37. data/lib/mihari/types.rb +21 -0
  38. data/lib/mihari/version.rb +1 -1
  39. data/lib/mihari/web/app.rb +2 -0
  40. data/lib/mihari/web/controllers/alerts_controller.rb +3 -4
  41. data/lib/mihari/web/controllers/artifacts_controller.rb +46 -2
  42. data/lib/mihari/web/controllers/ip_address_controller.rb +36 -0
  43. data/lib/mihari/web/controllers/sources_controller.rb +2 -2
  44. data/lib/mihari/web/controllers/tags_controller.rb +3 -1
  45. data/lib/mihari/web/public/index.html +1 -1
  46. data/lib/mihari/web/public/redoc-static.html +12 -10
  47. data/lib/mihari/web/public/static/fonts/fa-brands-400.1a575a41.woff +0 -0
  48. data/lib/mihari/web/public/static/fonts/fa-brands-400.513aa607.ttf +0 -0
  49. data/lib/mihari/web/public/static/fonts/fa-brands-400.592643a8.eot +0 -0
  50. data/lib/mihari/web/public/static/fonts/fa-brands-400.ed311c7a.woff2 +0 -0
  51. data/lib/mihari/web/public/static/fonts/fa-regular-400.766913e6.ttf +0 -0
  52. data/lib/mihari/web/public/static/fonts/fa-regular-400.b0e2db3b.eot +0 -0
  53. data/lib/mihari/web/public/static/fonts/fa-regular-400.b91d376b.woff2 +0 -0
  54. data/lib/mihari/web/public/static/fonts/fa-regular-400.d1d7e3b4.woff +0 -0
  55. data/lib/mihari/web/public/static/fonts/fa-solid-900.0c6bfc66.eot +0 -0
  56. data/lib/mihari/web/public/static/fonts/fa-solid-900.b9625119.ttf +0 -0
  57. data/lib/mihari/web/public/static/fonts/fa-solid-900.d745348d.woff +0 -0
  58. data/lib/mihari/web/public/static/fonts/fa-solid-900.d824df7e.woff2 +0 -0
  59. data/lib/mihari/web/public/static/img/fa-brands-400.1d5619cd.svg +3717 -0
  60. data/lib/mihari/web/public/static/img/fa-regular-400.c5d109be.svg +801 -0
  61. data/lib/mihari/web/public/static/img/fa-solid-900.37bc7099.svg +5034 -0
  62. data/lib/mihari/web/public/static/js/app.8e3e5150.js +36 -0
  63. data/lib/mihari/web/public/static/js/app.8e3e5150.js.map +1 -0
  64. data/lib/mihari/web/public/static/js/app.b5914c39.js +36 -0
  65. data/lib/mihari/web/public/static/js/app.b5914c39.js.map +1 -0
  66. data/lib/mihari.rb +25 -4
  67. data/mihari.gemspec +9 -2
  68. metadata +140 -8
@@ -20,10 +20,12 @@ module Mihari
20
20
  end
21
21
 
22
22
  #
23
- # Validate rule schema
23
+ # Validate rule schema and return a normalized rule
24
24
  #
25
25
  # @param [Hash] rule
26
26
  #
27
+ # @return [Hash]
28
+ #
27
29
  def validate_rule(rule)
28
30
  error_message = "Failed to parse the input as a rule!"
29
31
 
@@ -42,6 +44,8 @@ module Mihari
42
44
  puts error_message.colorize(:red)
43
45
  raise ArgumentError, "Invalid rule schema"
44
46
  end
47
+
48
+ result.to_h
45
49
  end
46
50
 
47
51
  #
@@ -18,8 +18,8 @@ module Mihari
18
18
  # @param [String, nil] source
19
19
  # @param [String, nil] tag_name
20
20
  # @param [String, nil] title
21
- # @param [String, nil] from_at
22
- # @param [String, nil] to_at
21
+ # @param [DateTime, nil] from_at
22
+ # @param [DateTime, nil] to_at
23
23
  # @param [Integer, nil] limit
24
24
  # @param [Integer, nil] page
25
25
  #
@@ -34,12 +34,22 @@ module Mihari
34
34
 
35
35
  offset = (page - 1) * limit
36
36
 
37
- relation = build_relation(artifact_data: artifact_data, title: title, description: description, source: source, tag_name: tag_name, from_at: from_at, to_at: to_at)
37
+ relation = build_relation(
38
+ artifact_data: artifact_data,
39
+ title: title,
40
+ description: description,
41
+ source: source,
42
+ tag_name: tag_name,
43
+ from_at: from_at,
44
+ to_at: to_at
45
+ )
38
46
 
39
- alerts = relation.limit(limit).offset(offset).order(id: :desc)
47
+ # TODO: improve queires
48
+ alert_ids = relation.limit(limit).offset(offset).order(id: :desc).pluck(:id).uniq
49
+ alerts = includes(:artifacts, :tags).where(id: [alert_ids]).order(id: :desc)
40
50
 
41
51
  alerts.map do |alert|
42
- json = AlertSerializer.new(alert).as_json
52
+ json = Serializers::AlertSerializer.new(alert).as_json
43
53
  json[:artifacts] = json[:artifacts] || []
44
54
  json[:tags] = json[:tags] || []
45
55
  json
@@ -54,13 +64,21 @@ module Mihari
54
64
  # @param [String, nil] source
55
65
  # @param [String, nil] tag_name
56
66
  # @param [String, nil] title
57
- # @param [String, nil] from_at
58
- # @param [String, nil] to_at
67
+ # @param [DateTime, nil] from_at
68
+ # @param [DateTime, nil] to_at
59
69
  #
60
70
  # @return [Integer]
61
71
  #
62
72
  def count(artifact_data: nil, description: nil, source: nil, tag_name: nil, title: nil, from_at: nil, to_at: nil)
63
- relation = build_relation(artifact_data: artifact_data, title: title, description: description, source: source, tag_name: tag_name, from_at: from_at, to_at: to_at)
73
+ relation = build_relation(
74
+ artifact_data: artifact_data,
75
+ title: title,
76
+ description: description,
77
+ source: source,
78
+ tag_name: tag_name,
79
+ from_at: from_at,
80
+ to_at: to_at
81
+ )
64
82
  relation.distinct("alerts.id").count
65
83
  end
66
84
 
@@ -68,8 +86,8 @@ module Mihari
68
86
 
69
87
  def build_relation(artifact_data: nil, title: nil, description: nil, source: nil, tag_name: nil, from_at: nil, to_at: nil)
70
88
  relation = self
71
- relation = joins(:tags) if tag_name
72
- relation = joins(:artifacts) if artifact_data
89
+
90
+ relation = relation.includes(:artifacts, :tags)
73
91
 
74
92
  relation = relation.where(artifacts: { data: artifact_data }) if artifact_data
75
93
  relation = relation.where(tags: { name: tag_name }) if tag_name
@@ -4,6 +4,7 @@ require "active_record"
4
4
  require "active_record/filter"
5
5
  require "active_support/core_ext/integer/time"
6
6
  require "active_support/core_ext/numeric/time"
7
+ require "uri"
7
8
 
8
9
  class ArtifactValidator < ActiveModel::Validator
9
10
  def validate(record)
@@ -15,6 +16,13 @@ end
15
16
 
16
17
  module Mihari
17
18
  class Artifact < ActiveRecord::Base
19
+ has_one :autonomous_system, dependent: :destroy
20
+ has_one :geolocation, dependent: :destroy
21
+ has_one :whois_record, dependent: :destroy
22
+
23
+ has_many :dns_records, dependent: :destroy
24
+ has_many :reverse_dns_names, dependent: :destroy
25
+
18
26
  include ActiveModel::Validations
19
27
 
20
28
  validates_with ArtifactValidator
@@ -44,5 +52,52 @@ module Mihari
44
52
  # within {ignore_threshold} days, do not ignore it
45
53
  artifact.created_at < days_before
46
54
  end
55
+
56
+ #
57
+ # Enrich(add) whois record
58
+ #
59
+ def enrich_whois
60
+ return unless can_enrich_whois?
61
+
62
+ self.whois_record = WhoisRecord.build_by_domain(normalize_as_domain(data))
63
+ end
64
+
65
+ #
66
+ # Enrich(add) DNS records
67
+ #
68
+ def enrich_dns
69
+ return unless can_enrich_dns?
70
+
71
+ self.dns_records = DnsRecord.build_by_domain(normalize_as_domain(data))
72
+ end
73
+
74
+ #
75
+ # Enrich(add) reverse DNS names
76
+ #
77
+ def enrich_reverse_dns
78
+ return unless can_enrich_revese_dns?
79
+
80
+ self.reverse_dns_names = ReverseDnsName.build_by_ip(data)
81
+ end
82
+
83
+ private
84
+
85
+ def normalize_as_domain(url_or_domain)
86
+ return url_or_domain if data_type == "domain"
87
+
88
+ URI.parse(url_or_domain).host
89
+ end
90
+
91
+ def can_enrich_whois?
92
+ %w[domain url].include? data_type
93
+ end
94
+
95
+ def can_enrich_dns?
96
+ %w[domain url].include? data_type
97
+ end
98
+
99
+ def can_enrich_revese_dns?
100
+ data_type == "ip"
101
+ end
47
102
  end
48
103
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+
5
+ module Mihari
6
+ class AutonomousSystem < ActiveRecord::Base
7
+ has_one :artifact, dependent: :destroy
8
+ end
9
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "resolv"
5
+
6
+ module Mihari
7
+ class DnsRecord < ActiveRecord::Base
8
+ class << self
9
+ #
10
+ # Build DNS records
11
+ #
12
+ # @param [String] domain
13
+ #
14
+ # @return [Array<Mihari::DnsRecord>]
15
+ #
16
+ def build_by_domain(domain)
17
+ resource_types = [
18
+ Resolv::DNS::Resource::IN::A,
19
+ Resolv::DNS::Resource::IN::AAAA,
20
+ Resolv::DNS::Resource::IN::CNAME,
21
+ Resolv::DNS::Resource::IN::TXT,
22
+ Resolv::DNS::Resource::IN::NS
23
+ ]
24
+
25
+ resource_types.map do |resource_type|
26
+ get_values domain, resource_type
27
+ rescue Resolv::ResolvError
28
+ nil
29
+ end.flatten.compact
30
+ end
31
+
32
+ private
33
+
34
+ def get_values(domain, resource_type)
35
+ resources = Resolv::DNS.new.getresources(domain, resource_type)
36
+ resource_name = resource_type.to_s.split("::").last
37
+
38
+ resources.map do |resource|
39
+ # A, AAAA
40
+ if resource.respond_to?(:address)
41
+ new(resource: resource_name, value: resource.address.to_s)
42
+ # CNAME, NS
43
+ elsif resource.respond_to?(:name)
44
+ new(resource: resource_name, value: resource.name.to_s)
45
+ # TXT
46
+ elsif resource.respond_to?(:data)
47
+ new(resource: resource_name, value: resource.data.to_s)
48
+ end
49
+ end.compact
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+
5
+ module Mihari
6
+ class Geolocation < ActiveRecord::Base
7
+ has_one :artifact, dependent: :destroy
8
+ end
9
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "resolv"
5
+
6
+ module Mihari
7
+ class ReverseDnsName < ActiveRecord::Base
8
+ class << self
9
+ #
10
+ # Build reverse DNS names
11
+ #
12
+ # @param [String] ip
13
+ #
14
+ # @return [Array<Mihari::ReverseDnsName>]
15
+ #
16
+ def build_by_ip(ip)
17
+ names = Resolv.getnames(ip)
18
+ names.map { |name| new(name: name) }
19
+ rescue Resolv::ResolvError
20
+ []
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "whois-parser"
5
+ require "public_suffix"
6
+
7
+ module Mihari
8
+ class WhoisRecord < ActiveRecord::Base
9
+ has_one :artifact, dependent: :destroy
10
+
11
+ @memo = {}
12
+
13
+ class << self
14
+ #
15
+ # Build whois record
16
+ #
17
+ # @param [Stinrg] domain
18
+ #
19
+ # @return [WhoisRecord, nil]
20
+ #
21
+ def build_by_domain(domain)
22
+ domain = PublicSuffix.domain(domain)
23
+
24
+ # check memo
25
+ if @memo.key?(domain)
26
+ whois_record = @memo[domain]
27
+ # return clone of the record
28
+ return whois_record.dup
29
+ end
30
+
31
+ record = Whois.whois(domain)
32
+ parser = record.parser
33
+
34
+ return nil if parser.available?
35
+
36
+ whois_record = new(
37
+ domain: domain,
38
+ created_on: get_created_on(parser),
39
+ updated_on: get_updated_on(parser),
40
+ expires_on: get_expires_on(parser),
41
+ registrar: get_registrar(parser),
42
+ contacts: get_contacts(parser)
43
+ )
44
+ # set memo
45
+ @memo[domain] = whois_record
46
+ whois_record
47
+ rescue Whois::Error, Whois::ParserError, Timeout::Error
48
+ nil
49
+ end
50
+
51
+ private
52
+
53
+ #
54
+ # Get created_on
55
+ #
56
+ # @param [::Whois::Parser:] parser
57
+ #
58
+ # @return [Date, nil]
59
+ #
60
+ def get_created_on(parser)
61
+ parser.created_on
62
+ rescue ::Whois::AttributeNotImplemented
63
+ nil
64
+ end
65
+
66
+ #
67
+ # Get updated_on
68
+ #
69
+ # @param [::Whois::Parser:] parser
70
+ #
71
+ # @return [Date, nil]
72
+ #
73
+ def get_updated_on(parser)
74
+ parser.updated_on
75
+ rescue ::Whois::AttributeNotImplemented
76
+ nil
77
+ end
78
+
79
+ #
80
+ # Get expires_on
81
+ #
82
+ # @param [::Whois::Parser:] parser
83
+ #
84
+ # @return [Date, nil]
85
+ #
86
+ def get_expires_on(parser)
87
+ parser.expires_on
88
+ rescue ::Whois::AttributeNotImplemented
89
+ nil
90
+ end
91
+
92
+ #
93
+ # Get registrar
94
+ #
95
+ # @param [::Whois::Parser:] parser
96
+ #
97
+ # @return [Hash, nil]
98
+ #
99
+ def get_registrar(parser)
100
+ parser.registrar&.to_h
101
+ rescue ::Whois::AttributeNotImplemented
102
+ nil
103
+ end
104
+
105
+ #
106
+ # Get contacts
107
+ #
108
+ # @param [::Whois::Parser:] parser
109
+ #
110
+ # @return [Array[Hash], nil]
111
+ #
112
+ def get_contacts(parser)
113
+ parser.contacts.map(&:to_h)
114
+ rescue ::Whois::AttributeNotImplemented
115
+ nil
116
+ end
117
+ end
118
+ end
119
+ end
@@ -13,6 +13,7 @@ module Mihari
13
13
  optional(:censys_secret).value(:string)
14
14
  optional(:circl_passive_password).value(:string)
15
15
  optional(:circl_passive_username).value(:string)
16
+ optional(:ipinfo_api_key).value(:string)
16
17
  optional(:misp_api_endpoint).value(:string)
17
18
  optional(:misp_api_key).value(:string)
18
19
  optional(:onyphe_api_key).value(:string)
@@ -2,26 +2,13 @@
2
2
 
3
3
  require "dry/schema"
4
4
  require "dry/validation"
5
- require "dry/types"
6
5
 
7
6
  require "mihari/schemas/macros"
8
7
 
9
8
  module Mihari
10
- module Types
11
- include Dry.Types()
12
- end
13
-
14
- DataTypes = Types::String.enum(*ALLOWED_DATA_TYPES)
15
-
16
- AnalyzerTypes = Types::String.enum(
17
- "binaryedge", "censys", "circl", "dnpedia", "dnstwister",
18
- "onyphe", "otx", "passivetotal", "pulsedive", "securitytrails",
19
- "shodan", "virustotal"
20
- )
21
-
22
9
  module Schemas
23
10
  Analyzer = Dry::Schema.Params do
24
- required(:analyzer).value(AnalyzerTypes)
11
+ required(:analyzer).value(Types::AnalyzerTypes)
25
12
  required(:query).value(:string)
26
13
  end
27
14
 
@@ -62,8 +49,11 @@ module Mihari
62
49
 
63
50
  required(:queries).value(:array).each { Analyzer | Spyse | ZoomEye | Urlscan | Crtsh }
64
51
 
65
- optional(:allowed_data_types).value(array[DataTypes]).default(ALLOWED_DATA_TYPES)
52
+ optional(:allowed_data_types).value(array[Types::DataTypes]).default(ALLOWED_DATA_TYPES)
66
53
  optional(:disallowed_data_values).value(array[:string]).default([])
54
+
55
+ optional(:ignore_old_artifacts).value(:bool).default(false)
56
+ optional(:ignore_threshold).value(:integer).default(0)
67
57
  end
68
58
 
69
59
  class RuleContract < Dry::Validation::Contract
@@ -3,10 +3,12 @@
3
3
  require "active_model_serializers"
4
4
 
5
5
  module Mihari
6
- class AlertSerializer < ActiveModel::Serializer
7
- attributes :id, :title, :description, :source, :created_at
6
+ module Serializers
7
+ class AlertSerializer < ActiveModel::Serializer
8
+ attributes :id, :title, :description, :source, :created_at
8
9
 
9
- has_many :artifacts
10
- has_many :tags, through: :taggings
10
+ has_many :artifacts, serializer: ArtifactSerializer
11
+ has_many :tags, through: :taggings, serializer: TagSerializer
12
+ end
11
13
  end
12
14
  end