mihari 4.5.3 → 4.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02c4e60250ffc8e40e4d0c08fd3721101ab943abdfeb1f73ec267cc345bc0dd2
4
- data.tar.gz: 5e65063ca7b9cc8c5b9c6d137abe6820abb6b179c3fb8582d1381d583eb27280
3
+ metadata.gz: '091892853042ab3f1010c89b943eb2a885370da2d619016f3f3cd9dcb59e80cf'
4
+ data.tar.gz: 119ccbb407a49c741a4bf6c7cfafc7cd99855c3af950af9d0f1dc3735fc24672
5
5
  SHA512:
6
- metadata.gz: 142cc9ec86582d37e7e5bd472f1a4f085582441e2204d406fa6ff9d2e94331cbc27513a410c00174950750e066ff9c53b100bd4cfe23039e4cb2be7d8f69ed33
7
- data.tar.gz: 49a02b02e298b94d8d847d74ea3389a3e873e72e17fdb93d1e5392992be05db73b37f6cea05ead35b04573400b6956e5b34d323008efb679fba60dcf53d6b372
6
+ metadata.gz: 5364837634a6cde1b370db5613e745f05b40bf7321a5b7fcc4a2c7d32b1d395fa45e4472ef2a5f9f28d664f0b45e31d0554acec7bb641a21ce179ebf70bf9317
7
+ data.tar.gz: 57ca2f920db2da9e8c9a10f59aa81984dff0a81f0073ba839c351f3fe5e7979358ce44d87f48adcffd7df1bf7268bab178fec82731f72d62c24aa9ce04eed026
@@ -51,6 +51,7 @@ module Mihari
51
51
  option :disallowed_data_values, default: proc { [] }
52
52
 
53
53
  option :emitters, optional: true
54
+ option :enrichers, optional: true
54
55
 
55
56
  attr_reader :source
56
57
 
@@ -60,6 +61,7 @@ module Mihari
60
61
  @source = id
61
62
 
62
63
  @emitters = emitters || DEFAULT_EMITTERS
64
+ @enrichers = enrichers || DEFAULT_ENRICHERS
63
65
 
64
66
  validate_analyzer_configurations
65
67
  end
@@ -112,6 +114,21 @@ module Mihari
112
114
  end
113
115
  end
114
116
 
117
+ #
118
+ # Enriched artifacts
119
+ #
120
+ # @return [Array<Mihari::Artifact>]
121
+ #
122
+ def enriched_artifacts
123
+ @enriched_artifacts ||= Parallel.map(unique_artifacts) do |artifact|
124
+ enrichers.each do |enricher|
125
+ artifact.enrich_by_enricher(enricher[:enricher])
126
+ end
127
+
128
+ artifact
129
+ end
130
+ end
131
+
115
132
  #
116
133
  # Normalized disallowed data values
117
134
  #
@@ -4,4 +4,6 @@ module Mihari
4
4
  ALLOWED_DATA_TYPES = ["hash", "ip", "domain", "url", "mail"].freeze
5
5
 
6
6
  DEFAULT_EMITTERS = ["database", "misp", "slack", "the_hive", "webhook"].map { |name| { emitter: name } }.freeze
7
+
8
+ DEFAULT_ENRICHERS = ["whois", "ipinfo", "shodan", "google_public_dns"].map { |name| { enricher: name } }.freeze
7
9
  end
@@ -11,11 +11,15 @@ module Mihari
11
11
  # @return [String, nil]
12
12
  attr_reader :api_key
13
13
 
14
+ # @return [String, nil]
15
+ attr_reader :api_version
16
+
14
17
  def initialize(*args, **kwargs)
15
18
  super(*args, **kwargs)
16
19
 
17
20
  @api_endpoint = kwargs[:api_endpoint] || Mihari.config.thehive_api_endpoint
18
21
  @api_key = kwargs[:api_key] || Mihari.config.thehive_api_key
22
+ @api_version = kwargs[:api_version] || Mihari.config.thehive_api_version
19
23
  end
20
24
 
21
25
  # @return [Boolean]
@@ -26,14 +30,28 @@ module Mihari
26
30
  def emit(title:, description:, artifacts:, tags: [], **_options)
27
31
  return if artifacts.empty?
28
32
 
29
- api.alert.create(
30
- title: title,
31
- description: description,
32
- artifacts: artifacts.map { |artifact| { data: artifact.data, data_type: artifact.data_type, message: description } },
33
- tags: tags,
34
- type: "external",
35
- source: "mihari"
36
- )
33
+ payload = payload(title: title, description: description, artifacts: artifacts, tags: tags)
34
+ api.alert.create(**payload)
35
+ end
36
+
37
+ #
38
+ # Normalize API version for API client
39
+ #
40
+ # @param [String] version
41
+ #
42
+ # @return [String, nil]
43
+ #
44
+ def normalized_api_version
45
+ @normalized_api_version ||= [].tap do |out|
46
+ # v4 does not have version prefix in path (/api/)
47
+ # v5 has version prefix in path (/api/v1/)
48
+ table = {
49
+ "" => nil,
50
+ "v4" => nil,
51
+ "v5" => "v1"
52
+ }
53
+ out << table[api_version.to_s.downcase]
54
+ end.first
37
55
  end
38
56
 
39
57
  private
@@ -43,7 +61,7 @@ module Mihari
43
61
  end
44
62
 
45
63
  def api
46
- @api ||= Hachi::API.new(api_endpoint: api_endpoint, api_key: api_key)
64
+ @api ||= Hachi::API.new(api_endpoint: api_endpoint, api_key: api_key, api_version: normalized_api_version)
47
65
  end
48
66
 
49
67
  #
@@ -64,6 +82,35 @@ module Mihari
64
82
  !api_key.nil?
65
83
  end
66
84
 
85
+ def payload(title:, description:, artifacts:, tags: [])
86
+ return v4_payload(title: title, description: description, artifacts: artifacts, tags: tags) if normalized_api_version.nil?
87
+
88
+ v5_payload(title: title, description: description, artifacts: artifacts, tags: tags)
89
+ end
90
+
91
+ def v4_payload(title:, description:, artifacts:, tags: [])
92
+ {
93
+ title: title,
94
+ description: description,
95
+ artifacts: artifacts.map { |artifact| { data: artifact.data, data_type: artifact.data_type, message: description } },
96
+ tags: tags,
97
+ type: "external",
98
+ source: "mihari"
99
+ }
100
+ end
101
+
102
+ def v5_payload(title:, description:, artifacts:, tags: [])
103
+ {
104
+ title: title,
105
+ description: description,
106
+ observables: artifacts.map { |artifact| { data: artifact.data, data_type: artifact.data_type, message: description } },
107
+ tags: tags,
108
+ type: "external",
109
+ source: "mihari",
110
+ source_ref: "1"
111
+ }
112
+ end
113
+
67
114
  #
68
115
  # Check whether an API endpoint is reachable or not
69
116
  #
@@ -71,9 +118,21 @@ module Mihari
71
118
  #
72
119
  def ping?
73
120
  base_url = api_endpoint.end_with?("/") ? api_endpoint[0..-2] : api_endpoint
74
- url = "#{base_url}/index.html"
121
+
122
+ if normalized_api_version.nil?
123
+ # for v4
124
+ base_url = api_endpoint.end_with?("/") ? api_endpoint[0..-2] : api_endpoint
125
+ url = "#{base_url}/index.html"
126
+ else
127
+ # for v5
128
+ url = "#{base_url}/api/v1/status/public"
129
+ end
75
130
 
76
131
  http = Net::Ping::HTTP.new(url)
132
+
133
+ # use GET for v5
134
+ http.get_request = true if normalized_api_version
135
+
77
136
  http.ping?
78
137
  end
79
138
  end
@@ -11,7 +11,7 @@ module Mihari
11
11
  def emit(title:, description:, artifacts:, source:, tags:)
12
12
  return if artifacts.empty?
13
13
 
14
- headers = { 'content-type': "application/x-www-form-urlencoded" }
14
+ headers = { "content-type": "application/x-www-form-urlencoded" }
15
15
  headers["content-type"] = "application/json" if use_json_body?
16
16
 
17
17
  emitter = Emitters::HTTP.new(uri: Mihari.config.webhook_url)
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/https"
4
+
5
+ module Mihari
6
+ module Enrichers
7
+ class GooglePublicDNS < Base
8
+ # @return [Boolean]
9
+ def valid?
10
+ true
11
+ end
12
+
13
+ class << self
14
+ #
15
+ # Query Google Public DNS
16
+ #
17
+ # @param [String] name
18
+ # @param [String] resource_type
19
+ #
20
+ # @return [Mihari::Structs::Shodan::GooglePublicDNS::Response, nil]
21
+ #
22
+ def query(name, resource_type)
23
+ url = "https://dns.google/resolve"
24
+ params = { name: name, type: resource_type }
25
+ res = HTTP.get(url, params: params)
26
+
27
+ data = JSON.parse(res.body.to_s)
28
+
29
+ Structs::GooglePublicDNS::Response.from_dynamic! data
30
+ rescue HTTPError
31
+ nil
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "whois-parser"
4
+
5
+ module Mihari
6
+ module Enrichers
7
+ class Whois < Base
8
+ @memo = {}
9
+
10
+ # @return [Boolean]
11
+ def valid?
12
+ true
13
+ end
14
+
15
+ class << self
16
+ #
17
+ # Query IAIA Whois API
18
+ #
19
+ # @param [String] name
20
+ #
21
+ # @return [Mihari::WhoisRecord, nil]
22
+ #
23
+ def query(domain)
24
+ domain = PublicSuffix.domain(domain)
25
+
26
+ # check memo
27
+ if @memo.key?(domain)
28
+ whois_record = @memo[domain]
29
+ # return clone of the record
30
+ return whois_record.dup
31
+ end
32
+
33
+ record = ::Whois.whois(domain)
34
+ parser = record.parser
35
+
36
+ return nil if parser.available?
37
+
38
+ whois_record = WhoisRecord.new(
39
+ domain: domain,
40
+ created_on: get_created_on(parser),
41
+ updated_on: get_updated_on(parser),
42
+ expires_on: get_expires_on(parser),
43
+ registrar: get_registrar(parser),
44
+ contacts: get_contacts(parser)
45
+ )
46
+ # set memo
47
+ @memo[domain] = whois_record
48
+ whois_record
49
+ rescue ::Whois::Error, ::Whois::ParserError, Timeout::Error
50
+ nil
51
+ end
52
+
53
+ def reset_cache
54
+ @memo = {}
55
+ end
56
+
57
+ private
58
+
59
+ #
60
+ # Get created_on
61
+ #
62
+ # @param [::Whois::Parser:] parser
63
+ #
64
+ # @return [Date, nil]
65
+ #
66
+ def get_created_on(parser)
67
+ parser.created_on
68
+ rescue ::Whois::AttributeNotImplemented
69
+ nil
70
+ end
71
+
72
+ #
73
+ # Get updated_on
74
+ #
75
+ # @param [::Whois::Parser:] parser
76
+ #
77
+ # @return [Date, nil]
78
+ #
79
+ def get_updated_on(parser)
80
+ parser.updated_on
81
+ rescue ::Whois::AttributeNotImplemented
82
+ nil
83
+ end
84
+
85
+ #
86
+ # Get expires_on
87
+ #
88
+ # @param [::Whois::Parser:] parser
89
+ #
90
+ # @return [Date, nil]
91
+ #
92
+ def get_expires_on(parser)
93
+ parser.expires_on
94
+ rescue ::Whois::AttributeNotImplemented
95
+ nil
96
+ end
97
+
98
+ #
99
+ # Get registrar
100
+ #
101
+ # @param [::Whois::Parser:] parser
102
+ #
103
+ # @return [Hash, nil]
104
+ #
105
+ def get_registrar(parser)
106
+ parser.registrar&.to_h
107
+ rescue ::Whois::AttributeNotImplemented
108
+ nil
109
+ end
110
+
111
+ #
112
+ # Get contacts
113
+ #
114
+ # @param [::Whois::Parser:] parser
115
+ #
116
+ # @return [Array[Hash], nil]
117
+ #
118
+ def get_contacts(parser)
119
+ parser.contacts.map(&:to_h)
120
+ rescue ::Whois::AttributeNotImplemented
121
+ nil
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
data/lib/mihari/http.rb CHANGED
@@ -44,8 +44,8 @@ module Mihari
44
44
  end
45
45
 
46
46
  class << self
47
- def get(uri, headers: {}, payload: {})
48
- client = new(uri, headers: headers, payload: payload)
47
+ def get(uri, headers: {}, params: {})
48
+ client = new(uri, headers: headers, payload: params)
49
49
  client.get
50
50
  end
51
51
 
@@ -135,6 +135,36 @@ module Mihari
135
135
  enrich_cpes
136
136
  end
137
137
 
138
+ ENRICH_METHODS_BY_ENRICHER = {
139
+ whois: [
140
+ :enrich_whois
141
+ ],
142
+ ipinfo: [
143
+ :enrich_autonomous_system,
144
+ :enrich_geolocation
145
+ ],
146
+ shodan: [
147
+ :enrich_ports,
148
+ :enrich_cpes,
149
+ :enrich_reverse_dns
150
+ ],
151
+ google_public_dns: [
152
+ :enrich_dns
153
+ ]
154
+ }.freeze
155
+
156
+ #
157
+ # Enrich by name of enricher
158
+ #
159
+ # @param [String] enricher
160
+ #
161
+ def enrich_by_enricher(enricher)
162
+ methods = ENRICH_METHODS_BY_ENRICHER[enricher.downcase.to_sym] || []
163
+ methods.each do |method|
164
+ send(method) if respond_to?(method)
165
+ end
166
+ end
167
+
138
168
  private
139
169
 
140
170
  def normalize_as_domain(url_or_domain)
@@ -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.cpes.map { |cpe| new(cpe: cpe) }
20
20
  end
@@ -13,14 +13,7 @@ module Mihari
13
13
  # @return [Array<Mihari::DnsRecord>]
14
14
  #
15
15
  def build_by_domain(domain)
16
- resource_types = [
17
- Resolv::DNS::Resource::IN::A,
18
- Resolv::DNS::Resource::IN::AAAA,
19
- Resolv::DNS::Resource::IN::CNAME,
20
- Resolv::DNS::Resource::IN::TXT,
21
- Resolv::DNS::Resource::IN::NS
22
- ]
23
-
16
+ resource_types = %w[A AAAA CNAME TXT NS]
24
17
  resource_types.map do |resource_type|
25
18
  get_values domain, resource_type
26
19
  rescue Resolv::ResolvError
@@ -31,20 +24,11 @@ module Mihari
31
24
  private
32
25
 
33
26
  def get_values(domain, resource_type)
34
- resources = Resolv::DNS.new.getresources(domain, resource_type)
35
- resource_name = resource_type.to_s.split("::").last
27
+ response = Enrichers::GooglePublicDNS.query(domain, resource_type)
28
+ answers = response.answers || []
36
29
 
37
- resources.filter_map do |resource|
38
- # A, AAAA
39
- if resource.respond_to?(:address)
40
- new(resource: resource_name, value: resource.address.to_s)
41
- # CNAME, NS
42
- elsif resource.respond_to?(:name)
43
- new(resource: resource_name, value: resource.name.to_s)
44
- # TXT
45
- elsif resource.respond_to?(:data)
46
- new(resource: resource_name, value: resource.data.to_s)
47
- end
30
+ answers.filter_map do |answer|
31
+ new(resource: answer.resource_type, value: answer.data)
48
32
  end
49
33
  end
50
34
  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.hostnames.map { |name| new(name: name) }
20
20
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "whois-parser"
4
-
5
3
  module Mihari
6
4
  class WhoisRecord < ActiveRecord::Base
7
5
  belongs_to :artifact
@@ -17,100 +15,7 @@ module Mihari
17
15
  # @return [WhoisRecord, nil]
18
16
  #
19
17
  def build_by_domain(domain)
20
- domain = PublicSuffix.domain(domain)
21
-
22
- # check memo
23
- if @memo.key?(domain)
24
- whois_record = @memo[domain]
25
- # return clone of the record
26
- return whois_record.dup
27
- end
28
-
29
- record = Whois.whois(domain)
30
- parser = record.parser
31
-
32
- return nil if parser.available?
33
-
34
- whois_record = new(
35
- domain: domain,
36
- created_on: get_created_on(parser),
37
- updated_on: get_updated_on(parser),
38
- expires_on: get_expires_on(parser),
39
- registrar: get_registrar(parser),
40
- contacts: get_contacts(parser)
41
- )
42
- # set memo
43
- @memo[domain] = whois_record
44
- whois_record
45
- rescue Whois::Error, Whois::ParserError, Timeout::Error
46
- nil
47
- end
48
-
49
- private
50
-
51
- #
52
- # Get created_on
53
- #
54
- # @param [::Whois::Parser:] parser
55
- #
56
- # @return [Date, nil]
57
- #
58
- def get_created_on(parser)
59
- parser.created_on
60
- rescue ::Whois::AttributeNotImplemented
61
- nil
62
- end
63
-
64
- #
65
- # Get updated_on
66
- #
67
- # @param [::Whois::Parser:] parser
68
- #
69
- # @return [Date, nil]
70
- #
71
- def get_updated_on(parser)
72
- parser.updated_on
73
- rescue ::Whois::AttributeNotImplemented
74
- nil
75
- end
76
-
77
- #
78
- # Get expires_on
79
- #
80
- # @param [::Whois::Parser:] parser
81
- #
82
- # @return [Date, nil]
83
- #
84
- def get_expires_on(parser)
85
- parser.expires_on
86
- rescue ::Whois::AttributeNotImplemented
87
- nil
88
- end
89
-
90
- #
91
- # Get registrar
92
- #
93
- # @param [::Whois::Parser:] parser
94
- #
95
- # @return [Hash, nil]
96
- #
97
- def get_registrar(parser)
98
- parser.registrar&.to_h
99
- rescue ::Whois::AttributeNotImplemented
100
- nil
101
- end
102
-
103
- #
104
- # Get contacts
105
- #
106
- # @param [::Whois::Parser:] parser
107
- #
108
- # @return [Array[Hash], nil]
109
- #
110
- def get_contacts(parser)
111
- parser.contacts.map(&:to_h)
112
- rescue ::Whois::AttributeNotImplemented
113
- nil
18
+ Enrichers::Whois.query domain
114
19
  end
115
20
  end
116
21
  end
@@ -16,6 +16,7 @@ module Mihari
16
16
  required(:emitter).value(Types::String.enum("the_hive"))
17
17
  optional(:api_endpoint).value(:string)
18
18
  optional(:api_key).value(:string)
19
+ optional(:api_version).value(Types::String.enum("v4", "v5")).default("v4")
19
20
  end
20
21
 
21
22
  Slack = Dry::Schema.Params do
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Schemas
5
+ Enricher = Dry::Schema.Params do
6
+ required(:enricher).value(Types::EnricherTypes)
7
+ end
8
+ end
9
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "mihari/schemas/analyzer"
4
4
  require "mihari/schemas/emitter"
5
+ require "mihari/schemas/enricher"
5
6
 
6
7
  module Mihari
7
8
  module Schemas
@@ -20,6 +21,8 @@ module Mihari
20
21
 
21
22
  optional(:emitters).value(:array).each { Emitter | MISP | TheHive | Slack | HTTP }
22
23
 
24
+ optional(:enrichers).value(:array).each(Enricher)
25
+
23
26
  optional(:allowed_data_types).value(array[Types::DataTypes]).default(ALLOWED_DATA_TYPES)
24
27
  optional(:disallowed_data_values).value(array[:string]).default([])
25
28
 
@@ -35,6 +38,9 @@ module Mihari
35
38
  emitters = h[:emitters]
36
39
  h[:emitters] = emitters || DEFAULT_EMITTERS
37
40
 
41
+ enrichers = h[:enrichers]
42
+ h[:enrichers] = enrichers || DEFAULT_ENRICHERS
43
+
38
44
  h
39
45
  end
40
46
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Structs
5
+ module GooglePublicDNS
6
+ INT_TYPE_TO_TYPE = {
7
+ 1 => "A",
8
+ 2 => "NS",
9
+ 5 => "CNAME",
10
+ 16 => "TXT",
11
+ 28 => "AAAA"
12
+ }
13
+
14
+ class Answer < Dry::Struct
15
+ attribute :name, Types::String
16
+ attribute :data, Types::String
17
+ attribute :resource_type, Types::String
18
+
19
+ def self.from_dynamic!(d)
20
+ d = Types::Hash[d]
21
+ resource_type = INT_TYPE_TO_TYPE[d.fetch("type")]
22
+ new(
23
+ name: d.fetch("name"),
24
+ data: d.fetch("data"),
25
+ resource_type: resource_type
26
+ )
27
+ end
28
+ end
29
+
30
+ class Response < Dry::Struct
31
+ attribute :answers, Types.Array(Answer)
32
+
33
+ def self.from_dynamic!(d)
34
+ d = Types::Hash[d]
35
+ new(
36
+ answers: d.fetch("Answer", []).map { |x| Answer.from_dynamic!(x) }
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -88,7 +88,7 @@ module Mihari
88
88
 
89
89
  raise RuleValidationError, error_messages.join("\n") if errors?
90
90
 
91
- raise RuleValidationError, "Something wrong with queries or emitters." unless @no_method_error.nil?
91
+ raise RuleValidationError, "Something wrong with queries, emitters or enrichers." unless @no_method_error.nil?
92
92
  end
93
93
 
94
94
  def [](key)
@@ -150,6 +150,7 @@ module Mihari
150
150
  allowed_data_types: self[:allowed_data_types],
151
151
  disallowed_data_values: self[:disallowed_data_values],
152
152
  emitters: self[:emitters],
153
+ enrichers: self[:enrichers],
153
154
  id: id
154
155
  )
155
156
  analyzer.ignore_old_artifacts = self[:ignore_old_artifacts]
@@ -4,7 +4,7 @@ module Mihari
4
4
  module Structs
5
5
  module VirusTotalIntelligence
6
6
  class ContextAttributes < Dry::Struct
7
- attribute :url, Types.Array(Types::String).optional
7
+ attribute :url, Types::String.optional
8
8
 
9
9
  def self.from_dynamic!(d)
10
10
  d = Types::Hash[d]
@@ -25,7 +25,7 @@ module Mihari
25
25
  when "file"
26
26
  id
27
27
  when "url"
28
- (context_attributes.url || []).first
28
+ context_attributes&.url
29
29
  when "domain"
30
30
  id
31
31
  when "ip_address"
data/lib/mihari/types.rb CHANGED
@@ -21,5 +21,12 @@ module Mihari
21
21
  "database",
22
22
  "webhook"
23
23
  )
24
+
25
+ EnricherTypes = Types::String.enum(
26
+ "whois",
27
+ "ipinfo",
28
+ "shodan",
29
+ "google_public_dns"
30
+ )
24
31
  end
25
32
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mihari
4
- VERSION = "4.5.3"
4
+ VERSION = "4.7.0"
5
5
  end
@@ -53,6 +53,9 @@ module Mihari
53
53
 
54
54
  Rack::Handler::Puma.run(instance, Port: port, Host: host, Threads: threads, Verbose: verbose) do |_launcher|
55
55
  Launchy.open(url) if ENV["RACK_ENV"] != "development"
56
+ rescue Launchy::CommandNotFoundError
57
+ # ref. https://github.com/ninoseki/mihari/issues/477
58
+ # do nothing
56
59
  end
57
60
  end
58
61
  end
data/lib/mihari.rb CHANGED
@@ -71,34 +71,55 @@ end
71
71
  module Mihari
72
72
  extend Dry::Configurable
73
73
 
74
- setting :binaryedge_api_key, default: ENV["BINARYEDGE_API_KEY"]
75
- setting :censys_id, default: ENV["CENSYS_ID"]
76
- setting :censys_secret, default: ENV["CENSYS_SECRET"]
77
- setting :circl_passive_password, default: ENV["CIRCL_PASSIVE_PASSWORD"]
78
- setting :circl_passive_username, default: ENV["CIRCL_PASSIVE_USERNAME"]
79
- setting :database, default: ENV["DATABASE"] || "mihari.db"
80
- setting :greynoise_api_key, default: ENV["GREYNOISE_API_KEY"]
81
- setting :ipinfo_api_key, default: ENV["IPINFO_API_KEY"]
82
- setting :misp_api_endpoint, default: ENV["MISP_API_ENDPOINT"]
83
- setting :misp_api_key, default: ENV["MISP_API_KEY"]
84
- setting :onyphe_api_key, default: ENV["ONYPHE_API_KEY"]
85
- setting :otx_api_key, default: ENV["OTX_API_KEY"]
86
- setting :passivetotal_api_key, default: ENV["PASSIVETOTAL_API_KEY"]
87
- setting :passivetotal_username, default: ENV["PASSIVETOTAL_USERNAME"]
88
- setting :pulsedive_api_key, default: ENV["PULSEDIVE_API_KEY"]
89
- setting :securitytrails_api_key, default: ENV["SECURITYTRAILS_API_KEY"]
90
- setting :shodan_api_key, default: ENV["SHODAN_API_KEY"]
91
- setting :slack_channel, default: ENV["SLACK_CHANNEL"]
92
- setting :slack_webhook_url, default: ENV["SLACK_WEBHOOK_URL"]
93
- setting :spyse_api_key, default: ENV["SPYSE_API_KEY"]
94
- setting :thehive_api_endpoint, default: ENV["THEHIVE_API_ENDPOINT"]
95
- setting :thehive_api_key, default: ENV["THEHIVE_API_KEY"]
96
- setting :urlscan_api_key, default: ENV["URLSCAN_API_KEY"]
97
- setting :virustotal_api_key, default: ENV["VIRUSTOTAL_API_KEY"]
98
- setting :webhook_url, default: ENV["WEBHOOK_URL"]
99
- setting :webhook_use_json_body, constructor: ->(value = ENV["WEBHOOK_USE_JSON_BODY"]) { truthy?(value) }
100
- setting :zoomeye_api_key, default: ENV["ZOOMEYE_API_KEY"]
101
- setting :sentry_dsn, default: ENV["SENTRY_DSN"]
74
+ setting :binaryedge_api_key, default: ENV.fetch("BINARYEDGE_API_KEY", nil)
75
+
76
+ setting :censys_id, default: ENV.fetch("CENSYS_ID", nil)
77
+ setting :censys_secret, default: ENV.fetch("CENSYS_SECRET", nil)
78
+
79
+ setting :circl_passive_password, default: ENV.fetch("CIRCL_PASSIVE_PASSWORD", nil)
80
+ setting :circl_passive_username, default: ENV.fetch("CIRCL_PASSIVE_USERNAME", nil)
81
+
82
+ setting :database, default: ENV.fetch("DATABASE", "mihari.db")
83
+
84
+ setting :greynoise_api_key, default: ENV.fetch("GREYNOISE_API_KEY", nil)
85
+
86
+ setting :ipinfo_api_key, default: ENV.fetch("IPINFO_API_KEY", nil)
87
+
88
+ setting :misp_api_endpoint, default: ENV.fetch("MISP_API_ENDPOINT", nil)
89
+ setting :misp_api_key, default: ENV.fetch("MISP_API_KEY", nil)
90
+
91
+ setting :onyphe_api_key, default: ENV.fetch("ONYPHE_API_KEY", nil)
92
+
93
+ setting :otx_api_key, default: ENV.fetch("OTX_API_KEY", nil)
94
+
95
+ setting :passivetotal_api_key, default: ENV.fetch("PASSIVETOTAL_API_KEY", nil)
96
+ setting :passivetotal_username, default: ENV.fetch("PASSIVETOTAL_USERNAME", nil)
97
+
98
+ setting :pulsedive_api_key, default: ENV.fetch("PULSEDIVE_API_KEY", nil)
99
+
100
+ setting :securitytrails_api_key, default: ENV.fetch("SECURITYTRAILS_API_KEY", nil)
101
+
102
+ setting :shodan_api_key, default: ENV.fetch("SHODAN_API_KEY", nil)
103
+
104
+ setting :slack_channel, default: ENV.fetch("SLACK_CHANNEL", nil)
105
+ setting :slack_webhook_url, default: ENV.fetch("SLACK_WEBHOOK_URL", nil)
106
+
107
+ setting :spyse_api_key, default: ENV.fetch("SPYSE_API_KEY", nil)
108
+
109
+ setting :thehive_api_endpoint, default: ENV.fetch("THEHIVE_API_ENDPOINT", nil)
110
+ setting :thehive_api_key, default: ENV.fetch("THEHIVE_API_KEY", nil)
111
+ setting :thehive_api_version, default: ENV.fetch("THEHIVE_API_VERSION", nil)
112
+
113
+ setting :urlscan_api_key, default: ENV.fetch("URLSCAN_API_KEY", nil)
114
+
115
+ setting :virustotal_api_key, default: ENV.fetch("VIRUSTOTAL_API_KEY", nil)
116
+
117
+ setting :webhook_url, default: ENV.fetch("WEBHOOK_URL", nil)
118
+ setting :webhook_use_json_body, constructor: ->(value = ENV.fetch("WEBHOOK_USE_JSON_BODY", nil)) { truthy?(value) }
119
+
120
+ setting :zoomeye_api_key, default: ENV.fetch("ZOOMEYE_API_KEY", nil)
121
+
122
+ setting :sentry_dsn, default: ENV.fetch("SENTRY_DSN", nil)
102
123
 
103
124
  class << self
104
125
  include Memist::Memoizable
@@ -152,6 +173,7 @@ require "mihari/types"
152
173
  # Structs
153
174
  require "mihari/structs/alert"
154
175
  require "mihari/structs/censys"
176
+ require "mihari/structs/google_public_dns"
155
177
  require "mihari/structs/greynoise"
156
178
  require "mihari/structs/ipinfo"
157
179
  require "mihari/structs/onyphe"
@@ -168,8 +190,10 @@ require "mihari/schemas/rule"
168
190
 
169
191
  # Enrichers
170
192
  require "mihari/enrichers/base"
193
+ require "mihari/enrichers/google_public_dns"
171
194
  require "mihari/enrichers/ipinfo"
172
195
  require "mihari/enrichers/shodan"
196
+ require "mihari/enrichers/whois"
173
197
 
174
198
  # Models
175
199
  require "mihari/models/alert"
data/mihari.gemspec CHANGED
@@ -29,7 +29,7 @@ Gem::Specification.new do |spec|
29
29
 
30
30
  spec.add_development_dependency "bundler", "~> 2.3"
31
31
  spec.add_development_dependency "coveralls_reborn", "~> 0.24"
32
- spec.add_development_dependency "fakefs", "~> 1.4"
32
+ spec.add_development_dependency "fakefs", "~> 1.5"
33
33
  spec.add_development_dependency "mysql2", "~> 0.5"
34
34
  spec.add_development_dependency "overcommit", "~> 0.59"
35
35
  spec.add_development_dependency "pg", "~> 1.3"
@@ -39,13 +39,13 @@ Gem::Specification.new do |spec|
39
39
  spec.add_development_dependency "rerun", "~> 0.13"
40
40
  spec.add_development_dependency "rspec", "~> 3.11"
41
41
  spec.add_development_dependency "simplecov-lcov", "~> 0.8.0"
42
- spec.add_development_dependency "standard", "~> 1.11"
43
- spec.add_development_dependency "steep", "~> 0.52"
42
+ spec.add_development_dependency "standard", "~> 1.12"
43
+ spec.add_development_dependency "steep", "~> 1.0"
44
44
  spec.add_development_dependency "timecop", "~> 0.9"
45
45
  spec.add_development_dependency "vcr", "~> 6.1"
46
46
  spec.add_development_dependency "webmock", "~> 3.14"
47
47
 
48
- spec.add_dependency "activerecord", "7.0.2.4"
48
+ spec.add_dependency "activerecord", "7.0.3"
49
49
  spec.add_dependency "addressable", "2.8.0"
50
50
  spec.add_dependency "awrence", "2.0.1"
51
51
  spec.add_dependency "binaryedge", "0.1.0"
@@ -58,16 +58,16 @@ Gem::Specification.new do |spec|
58
58
  spec.add_dependency "dry-container", "0.9.0"
59
59
  spec.add_dependency "dry-files", "0.1.0"
60
60
  spec.add_dependency "dry-initializer", "3.1.1"
61
- spec.add_dependency "dry-schema", "1.9.1"
61
+ spec.add_dependency "dry-schema", "1.9.2"
62
62
  spec.add_dependency "dry-struct", "1.4.0"
63
- spec.add_dependency "dry-validation", "1.8.0"
64
- spec.add_dependency "email_address", "0.2.2"
63
+ spec.add_dependency "dry-validation", "1.8.1"
64
+ spec.add_dependency "email_address", "0.2.3"
65
65
  spec.add_dependency "grape", "1.6.2"
66
66
  spec.add_dependency "grape-entity", "0.10.1"
67
67
  spec.add_dependency "grape-swagger", "1.4.2"
68
68
  spec.add_dependency "grape-swagger-entity", "0.5.1"
69
69
  spec.add_dependency "greynoise", "0.1.1"
70
- spec.add_dependency "hachi", "1.0.0"
70
+ spec.add_dependency "hachi", "2.0.0"
71
71
  spec.add_dependency "insensitive_hash", "0.3.3"
72
72
  spec.add_dependency "jr-cli", "0.5.1"
73
73
  spec.add_dependency "launchy", "2.5.0"
@@ -84,12 +84,12 @@ Gem::Specification.new do |spec|
84
84
  spec.add_dependency "public_suffix", "4.0.7"
85
85
  spec.add_dependency "pulsedive", "0.1.5"
86
86
  spec.add_dependency "puma", "5.6.4"
87
- spec.add_dependency "rack", "2.2.3"
87
+ spec.add_dependency "rack", "2.2.3.1"
88
88
  spec.add_dependency "rack-contrib", "2.3.0"
89
89
  spec.add_dependency "rack-cors", "1.1.1"
90
90
  spec.add_dependency "securitytrails", "1.0.0"
91
- spec.add_dependency "semantic_logger", "4.10.0"
92
- spec.add_dependency "sentry-ruby", "5.3.0"
91
+ spec.add_dependency "semantic_logger", "4.11.0"
92
+ spec.add_dependency "sentry-ruby", "5.3.1"
93
93
  spec.add_dependency "shodanx", "0.2.1"
94
94
  spec.add_dependency "slack-notifier", "2.4.0"
95
95
  spec.add_dependency "spysex", "0.2.0"
@@ -5,11 +5,15 @@ module Mihari
5
5
 
6
6
  attr_reader api_key: String?
7
7
 
8
+ attr_reader api_version: String?
9
+
8
10
  # @return [true, false]
9
11
  def valid?: () -> bool
10
12
 
11
13
  def emit: (title: untyped title, description: untyped description, artifacts: untyped artifacts, ?tags: untyped tags, **untyped _options) -> (nil | untyped)
12
14
 
15
+ def normalized_api_version: () -> String?
16
+
13
17
  private
14
18
 
15
19
  def configuration_keys: () -> ::Array["thehive_api_endpoint" | "thehive_api_key"]
@@ -0,0 +1,18 @@
1
+ module Mihari
2
+ module Enrichers
3
+ class GooglePublicDNS < Base
4
+ # @return [Boolean]
5
+ def valid?: () -> true
6
+
7
+ #
8
+ # Query Google Public DNS
9
+ #
10
+ # @param [String] name
11
+ # @param [String] resource_type
12
+ #
13
+ # @return [Mihari::Structs::Shodan::GooglePublicDNS::Response, nil]
14
+ #
15
+ def self.query: (String name, String resource_type) -> Mihari::Structs::Shodan::GooglePublicDNS::Response?
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ module Mihari
2
+ module Structs
3
+ module GooglePublicDNS
4
+ INT_TYPE_TO_TYPE: { 1 => "A", 2 => "NS", 5 => "CNAME", 16 => "TXT", 28 => "AAAA" }
5
+
6
+ class Answer < Dry::Struct
7
+ attr_reader name: String
8
+ attr_reader data: String
9
+ attr_reader resource_type: String
10
+
11
+ def self.from_dynamic!: (Hash[(String | Symbol), untyped] d) -> Mihari::Structs::GooglePublicDNS::Answer
12
+ end
13
+
14
+ class Response < Dry::Struct
15
+ attr_reader answers: Array[Mihari::Structs::GooglePublicDNS::Answer]
16
+
17
+ def self.from_dynamic!: (Hash[(String | Symbol), untyped] d) -> Mihari::Structs::GooglePublicDNS::Response
18
+ end
19
+ end
20
+ end
21
+ end
data/sig/lib/mihari.rbs CHANGED
@@ -19,6 +19,7 @@ class Configuration
19
19
  attr_accessor spyse_api_key (): String?
20
20
  attr_accessor thehive_api_endpoint (): String?
21
21
  attr_accessor thehive_api_key (): String?
22
+ attr_accessor thehive_api_version (): String?
22
23
  attr_accessor urlscan_api_key (): String?
23
24
  attr_accessor virustotal_api_key (): String?
24
25
  attr_accessor zoomeye_api_key (): String?
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mihari
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.5.3
4
+ version: 4.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Manabu Niseki
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-05-08 00:00:00.000000000 Z
11
+ date: 2022-06-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '1.4'
47
+ version: '1.5'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '1.4'
54
+ version: '1.5'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: mysql2
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -184,28 +184,28 @@ dependencies:
184
184
  requirements:
185
185
  - - "~>"
186
186
  - !ruby/object:Gem::Version
187
- version: '1.11'
187
+ version: '1.12'
188
188
  type: :development
189
189
  prerelease: false
190
190
  version_requirements: !ruby/object:Gem::Requirement
191
191
  requirements:
192
192
  - - "~>"
193
193
  - !ruby/object:Gem::Version
194
- version: '1.11'
194
+ version: '1.12'
195
195
  - !ruby/object:Gem::Dependency
196
196
  name: steep
197
197
  requirement: !ruby/object:Gem::Requirement
198
198
  requirements:
199
199
  - - "~>"
200
200
  - !ruby/object:Gem::Version
201
- version: '0.52'
201
+ version: '1.0'
202
202
  type: :development
203
203
  prerelease: false
204
204
  version_requirements: !ruby/object:Gem::Requirement
205
205
  requirements:
206
206
  - - "~>"
207
207
  - !ruby/object:Gem::Version
208
- version: '0.52'
208
+ version: '1.0'
209
209
  - !ruby/object:Gem::Dependency
210
210
  name: timecop
211
211
  requirement: !ruby/object:Gem::Requirement
@@ -254,14 +254,14 @@ dependencies:
254
254
  requirements:
255
255
  - - '='
256
256
  - !ruby/object:Gem::Version
257
- version: 7.0.2.4
257
+ version: 7.0.3
258
258
  type: :runtime
259
259
  prerelease: false
260
260
  version_requirements: !ruby/object:Gem::Requirement
261
261
  requirements:
262
262
  - - '='
263
263
  - !ruby/object:Gem::Version
264
- version: 7.0.2.4
264
+ version: 7.0.3
265
265
  - !ruby/object:Gem::Dependency
266
266
  name: addressable
267
267
  requirement: !ruby/object:Gem::Requirement
@@ -436,14 +436,14 @@ dependencies:
436
436
  requirements:
437
437
  - - '='
438
438
  - !ruby/object:Gem::Version
439
- version: 1.9.1
439
+ version: 1.9.2
440
440
  type: :runtime
441
441
  prerelease: false
442
442
  version_requirements: !ruby/object:Gem::Requirement
443
443
  requirements:
444
444
  - - '='
445
445
  - !ruby/object:Gem::Version
446
- version: 1.9.1
446
+ version: 1.9.2
447
447
  - !ruby/object:Gem::Dependency
448
448
  name: dry-struct
449
449
  requirement: !ruby/object:Gem::Requirement
@@ -464,28 +464,28 @@ dependencies:
464
464
  requirements:
465
465
  - - '='
466
466
  - !ruby/object:Gem::Version
467
- version: 1.8.0
467
+ version: 1.8.1
468
468
  type: :runtime
469
469
  prerelease: false
470
470
  version_requirements: !ruby/object:Gem::Requirement
471
471
  requirements:
472
472
  - - '='
473
473
  - !ruby/object:Gem::Version
474
- version: 1.8.0
474
+ version: 1.8.1
475
475
  - !ruby/object:Gem::Dependency
476
476
  name: email_address
477
477
  requirement: !ruby/object:Gem::Requirement
478
478
  requirements:
479
479
  - - '='
480
480
  - !ruby/object:Gem::Version
481
- version: 0.2.2
481
+ version: 0.2.3
482
482
  type: :runtime
483
483
  prerelease: false
484
484
  version_requirements: !ruby/object:Gem::Requirement
485
485
  requirements:
486
486
  - - '='
487
487
  - !ruby/object:Gem::Version
488
- version: 0.2.2
488
+ version: 0.2.3
489
489
  - !ruby/object:Gem::Dependency
490
490
  name: grape
491
491
  requirement: !ruby/object:Gem::Requirement
@@ -562,14 +562,14 @@ dependencies:
562
562
  requirements:
563
563
  - - '='
564
564
  - !ruby/object:Gem::Version
565
- version: 1.0.0
565
+ version: 2.0.0
566
566
  type: :runtime
567
567
  prerelease: false
568
568
  version_requirements: !ruby/object:Gem::Requirement
569
569
  requirements:
570
570
  - - '='
571
571
  - !ruby/object:Gem::Version
572
- version: 1.0.0
572
+ version: 2.0.0
573
573
  - !ruby/object:Gem::Dependency
574
574
  name: insensitive_hash
575
575
  requirement: !ruby/object:Gem::Requirement
@@ -800,14 +800,14 @@ dependencies:
800
800
  requirements:
801
801
  - - '='
802
802
  - !ruby/object:Gem::Version
803
- version: 2.2.3
803
+ version: 2.2.3.1
804
804
  type: :runtime
805
805
  prerelease: false
806
806
  version_requirements: !ruby/object:Gem::Requirement
807
807
  requirements:
808
808
  - - '='
809
809
  - !ruby/object:Gem::Version
810
- version: 2.2.3
810
+ version: 2.2.3.1
811
811
  - !ruby/object:Gem::Dependency
812
812
  name: rack-contrib
813
813
  requirement: !ruby/object:Gem::Requirement
@@ -856,28 +856,28 @@ dependencies:
856
856
  requirements:
857
857
  - - '='
858
858
  - !ruby/object:Gem::Version
859
- version: 4.10.0
859
+ version: 4.11.0
860
860
  type: :runtime
861
861
  prerelease: false
862
862
  version_requirements: !ruby/object:Gem::Requirement
863
863
  requirements:
864
864
  - - '='
865
865
  - !ruby/object:Gem::Version
866
- version: 4.10.0
866
+ version: 4.11.0
867
867
  - !ruby/object:Gem::Dependency
868
868
  name: sentry-ruby
869
869
  requirement: !ruby/object:Gem::Requirement
870
870
  requirements:
871
871
  - - '='
872
872
  - !ruby/object:Gem::Version
873
- version: 5.3.0
873
+ version: 5.3.1
874
874
  type: :runtime
875
875
  prerelease: false
876
876
  version_requirements: !ruby/object:Gem::Requirement
877
877
  requirements:
878
878
  - - '='
879
879
  - !ruby/object:Gem::Version
880
- version: 5.3.0
880
+ version: 5.3.1
881
881
  - !ruby/object:Gem::Dependency
882
882
  name: shodanx
883
883
  requirement: !ruby/object:Gem::Requirement
@@ -1110,8 +1110,10 @@ files:
1110
1110
  - lib/mihari/emitters/the_hive.rb
1111
1111
  - lib/mihari/emitters/webhook.rb
1112
1112
  - lib/mihari/enrichers/base.rb
1113
+ - lib/mihari/enrichers/google_public_dns.rb
1113
1114
  - lib/mihari/enrichers/ipinfo.rb
1114
1115
  - lib/mihari/enrichers/shodan.rb
1116
+ - lib/mihari/enrichers/whois.rb
1115
1117
  - lib/mihari/entities/alert.rb
1116
1118
  - lib/mihari/entities/artifact.rb
1117
1119
  - lib/mihari/entities/autonomous_system.rb
@@ -1153,11 +1155,13 @@ files:
1153
1155
  - lib/mihari/models/whois.rb
1154
1156
  - lib/mihari/schemas/analyzer.rb
1155
1157
  - lib/mihari/schemas/emitter.rb
1158
+ - lib/mihari/schemas/enricher.rb
1156
1159
  - lib/mihari/schemas/macros.rb
1157
1160
  - lib/mihari/schemas/rule.rb
1158
1161
  - lib/mihari/status.rb
1159
1162
  - lib/mihari/structs/alert.rb
1160
1163
  - lib/mihari/structs/censys.rb
1164
+ - lib/mihari/structs/google_public_dns.rb
1161
1165
  - lib/mihari/structs/greynoise.rb
1162
1166
  - lib/mihari/structs/ipinfo.rb
1163
1167
  - lib/mihari/structs/onyphe.rb
@@ -1243,6 +1247,7 @@ files:
1243
1247
  - sig/lib/mihari/emitters/the_hive.rbs
1244
1248
  - sig/lib/mihari/emitters/webhook.rbs
1245
1249
  - sig/lib/mihari/enrichers/base.rbs
1250
+ - sig/lib/mihari/enrichers/google_public_dns.rbs
1246
1251
  - sig/lib/mihari/enrichers/ipinfo.rbs
1247
1252
  - sig/lib/mihari/errors.rbs
1248
1253
  - sig/lib/mihari/feed/parser.rbs
@@ -1272,6 +1277,7 @@ files:
1272
1277
  - sig/lib/mihari/status.rbs
1273
1278
  - sig/lib/mihari/structs/alert.rbs
1274
1279
  - sig/lib/mihari/structs/censys.rbs
1280
+ - sig/lib/mihari/structs/google_public_dns.rbs
1275
1281
  - sig/lib/mihari/structs/greynoise.rbs
1276
1282
  - sig/lib/mihari/structs/ipinfo.rbs
1277
1283
  - sig/lib/mihari/structs/onyphe.rbs