mihari 3.3.0 → 3.6.0

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 (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