mihari 7.4.0 → 7.5.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: 9ec4774493a408eb666c7a33e671c977f7c400356758aab00ba776b36910bc42
4
- data.tar.gz: bf0e0269c1e12d73b064d06ebf41e10686caeb66aaf70ec39f4e3ce7843bc51a
3
+ metadata.gz: 71b5cf7cdeb320c813848b90973908a3999c839f742b9307ae819c1fbf20829b
4
+ data.tar.gz: 18b06e35086d2888016d6d1fd7a61c9e62f72e31f6a384e0c1a96f4fde8ab592
5
5
  SHA512:
6
- metadata.gz: 6dff8b5b3bcd3098bb90e84f1d026325ca5a24d2cffb30761e2c243dfbc81bfef8093bd885ef7e9b07a2338ef2ca1cba10686fd7612a6261205c08f0b9258a15
7
- data.tar.gz: 6958e9d9e344b98c29209ce4d5501ded9b8584367cf5556c99e351913e0fe8800d829ccedb53704951a7b29eb5b73950e071ff2460e95ad067aff81dbef2c81d
6
+ metadata.gz: 2861d810f7ade8e177fd343e706c55a4ef64f734c3f19ae324a58d29c2992b3a11ccc8ad6d6735dff9831b15ce552305e3981c72b9326d55b83f621154f9d2f9
7
+ data.tar.gz: edf9f41d660252298cb5909eec731a6ca39968eab5d73c3d6eca3d55f8b452337ee22898427e9165312881058f6e2ba474d6f548aac922431c1b483b7a9c9746
@@ -87,11 +87,12 @@ module Mihari
87
87
  #
88
88
  # @param [String] path
89
89
  # @param [Hash, nil] json
90
+ # @param [Hash, nil] headers
90
91
  #
91
92
  # @return [Hash]
92
93
  #
93
- def post_json(path, json: {})
94
- res = http.post(url_for(path), json:)
94
+ def post_json(path, json: {}, headers: nil)
95
+ res = http.post(url_for(path), json:, headers: headers || {})
95
96
  JSON.parse res.body.to_s
96
97
  end
97
98
  end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "whois-parser"
4
+
5
+ module Mihari
6
+ module Clients
7
+ #
8
+ # Whois client
9
+ #
10
+ class Whois
11
+ # @return [Integer, nil]
12
+ attr_reader :timeout
13
+
14
+ # @return [::Whois::Client]
15
+ attr_reader :client
16
+
17
+ #
18
+ # @param [Integer, nil] timeout
19
+ #
20
+ def initialize(timeout: nil)
21
+ @timeout = timeout
22
+
23
+ @client = lambda do
24
+ return ::Whois::Client.new if timeout.nil?
25
+
26
+ ::Whois::Client.new(timeout:)
27
+ end.call
28
+ end
29
+
30
+ #
31
+ # Query IAIA Whois API
32
+ #
33
+ # @param [Mihari::Models::Artifact] artifact
34
+ #
35
+ # @param [Object] domain
36
+ def lookup(domain)
37
+ record = client.lookup(domain)
38
+ return if record.parser.available?
39
+
40
+ Models::WhoisRecord.new(
41
+ domain:,
42
+ created_on: get_created_on(record.parser),
43
+ updated_on: get_updated_on(record.parser),
44
+ expires_on: get_expires_on(record.parser),
45
+ registrar: get_registrar(record.parser),
46
+ contacts: get_contacts(record.parser)
47
+ )
48
+ end
49
+
50
+ private
51
+
52
+ #
53
+ # Get created_on
54
+ #
55
+ # @param [::Whois::Parser] parser
56
+ #
57
+ # @return [Date, nil]
58
+ #
59
+ def get_created_on(parser)
60
+ parser.created_on
61
+ rescue ::Whois::AttributeNotImplemented
62
+ nil
63
+ end
64
+
65
+ #
66
+ # Get updated_on
67
+ #
68
+ # @param [::Whois::Parser] parser
69
+ #
70
+ # @return [Date, nil]
71
+ #
72
+ def get_updated_on(parser)
73
+ parser.updated_on
74
+ rescue ::Whois::AttributeNotImplemented
75
+ nil
76
+ end
77
+
78
+ #
79
+ # Get expires_on
80
+ #
81
+ # @param [::Whois::Parser] parser
82
+ #
83
+ # @return [Date, nil]
84
+ #
85
+ def get_expires_on(parser)
86
+ parser.expires_on
87
+ rescue ::Whois::AttributeNotImplemented
88
+ nil
89
+ end
90
+
91
+ #
92
+ # Get registrar
93
+ #
94
+ # @param [::Whois::Parser] parser
95
+ #
96
+ # @return [Hash, nil]
97
+ #
98
+ def get_registrar(parser)
99
+ parser.registrar&.to_h
100
+ rescue ::Whois::AttributeNotImplemented
101
+ nil
102
+ end
103
+
104
+ #
105
+ # Get contacts
106
+ #
107
+ # @param [::Whois::Parser] parser
108
+ #
109
+ # @return [Array<Hash>, nil]
110
+ #
111
+ def get_contacts(parser)
112
+ parser.contacts.map(&:to_h)
113
+ rescue ::Whois::AttributeNotImplemented
114
+ nil
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Clients
5
+ #
6
+ # Yeti API client
7
+ #
8
+ class Yeti < Base
9
+ #
10
+ # @param [String] base_url
11
+ # @param [String, nil] api_key
12
+ # @param [Hash] headers
13
+ # @param [Integer, nil] timeout
14
+ #
15
+ def initialize(base_url, api_key:, headers: {}, timeout: nil)
16
+ raise(ArgumentError, "api_key is required") unless api_key
17
+
18
+ headers["x-yeti-apikey"] = api_key
19
+ super(base_url, headers:, timeout:)
20
+ end
21
+
22
+ def get_token
23
+ res = post_json("/api/v2/auth/api-token")
24
+ res["access_token"]
25
+ end
26
+
27
+ #
28
+ # @param [Hash] json
29
+ #
30
+ # @return [Hash]
31
+ #
32
+ def create_observables(json)
33
+ token = get_token
34
+ post_json("/api/v2/observables/bulk", json:, headers: {authorization: "Bearer #{token}"})
35
+ end
36
+ end
37
+ end
38
+ end
data/lib/mihari/config.rb CHANGED
@@ -34,6 +34,8 @@ module Mihari
34
34
  thehive_url: nil,
35
35
  urlscan_api_key: nil,
36
36
  virustotal_api_key: nil,
37
+ yeti_api_key: nil,
38
+ yeti_url: nil,
37
39
  zoomeye_api_key: nil,
38
40
  # sidekiq
39
41
  sidekiq_redis_url: nil,
@@ -123,6 +125,12 @@ module Mihari
123
125
  # @!attribute [r] virustotal_api_key
124
126
  # @return [String, nil]
125
127
 
128
+ # @!attribute [r] yeti_url
129
+ # @return [String, nil]
130
+
131
+ # @!attribute [r] yeti_api_key
132
+ # @return [String, nil]
133
+
126
134
  # @!attribute [r] zoomeye_api_key
127
135
  # @return [String, nil]
128
136
 
@@ -26,9 +26,7 @@ module Mihari
26
26
 
27
27
  # @return [Boolean]
28
28
  def ip?
29
- Try[IPAddr::InvalidAddressError] do
30
- IPAddr.new(data).to_s == data
31
- end.recover { false }.value!
29
+ Try[IPAddr::InvalidAddressError] { IPAddr.new(data).to_s == data }.recover { false }.value!
32
30
  end
33
31
 
34
32
  # @return [Boolean]
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Emitters
5
+ class Yeti < Base
6
+ # @return [String, nil]
7
+ attr_reader :url
8
+
9
+ # @return [String, nil]
10
+ attr_reader :api_key
11
+
12
+ # @return [Array<Mihari::Models::Artifact>]
13
+ attr_accessor :artifacts
14
+
15
+ #
16
+ # @param [Mihari::Rule] rule
17
+ # @param [Hash, nil] options
18
+ # @param [Hash] params
19
+ #
20
+ def initialize(rule:, options: nil, **params)
21
+ super(rule:, options:)
22
+
23
+ @url = params[:url] || Mihari.config.yeti_url
24
+ @api_key = params[:api_key] || Mihari.config.yeti_api_key
25
+
26
+ @artifacts = []
27
+ end
28
+
29
+ #
30
+ # @return [Boolean]
31
+ #
32
+ def configured?
33
+ api_key? && url?
34
+ end
35
+
36
+ #
37
+ # Create a Hive alert
38
+ #
39
+ # @param [Array<Mihari::Models::Artifact>] artifacts
40
+ #
41
+ def call(artifacts)
42
+ return if artifacts.empty?
43
+
44
+ @artifacts = artifacts
45
+
46
+ client.create_observables({observables:})
47
+ end
48
+
49
+ #
50
+ # @return [String]
51
+ #
52
+ def target
53
+ URI(url).host || "N/A"
54
+ end
55
+
56
+ private
57
+
58
+ def client
59
+ Clients::Yeti.new(url, api_key:, timeout:)
60
+ end
61
+
62
+ #
63
+ # Check whether a URL is set or not
64
+ #
65
+ # @return [Boolean]
66
+ #
67
+ def url?
68
+ !url.nil?
69
+ end
70
+
71
+ def acceptable_artifacts
72
+ artifacts.reject { |artifact| artifact.data_type == "mail" }
73
+ end
74
+
75
+ #
76
+ # @param [Mihari::Models::Artifact] artifact
77
+ #
78
+ # @return [Hash]
79
+ #
80
+ def artifact_to_observable(artifact)
81
+ convert_table = {
82
+ domain: "hostname",
83
+ ip: "ipv4"
84
+ }
85
+
86
+ type = lambda do
87
+ detailed_type = DataType.detailed_type(artifact.data)
88
+ convert_table[detailed_type.to_sym] || detailed_type || artifact.data_type
89
+ end.call
90
+
91
+ {
92
+ tags:,
93
+ type:,
94
+ value: artifact.data
95
+ }
96
+ end
97
+
98
+ def tags
99
+ @tags ||= rule.tags.map(&:name)
100
+ end
101
+
102
+ def observables
103
+ acceptable_artifacts.map { |artifact| artifact_to_observable(artifact) }
104
+ end
105
+ end
106
+ end
107
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "whois-parser"
4
-
5
3
  module Mihari
6
4
  module Enrichers
7
5
  #
@@ -18,22 +16,15 @@ module Mihari
18
16
  def call(artifact)
19
17
  return if artifact.domain.nil?
20
18
 
21
- domain = PublicSuffix.domain(artifact.domain)
22
- record = memoized_lookup(domain)
23
- return if record.parser.available?
24
-
25
- artifact.whois_record ||= Models::WhoisRecord.new(
26
- domain:,
27
- created_on: get_created_on(record.parser),
28
- updated_on: get_updated_on(record.parser),
29
- expires_on: get_expires_on(record.parser),
30
- registrar: get_registrar(record.parser),
31
- contacts: get_contacts(record.parser)
32
- )
19
+ artifact.whois_record ||= memoized_lookup(PublicSuffix.domain(artifact.domain))
33
20
  end
34
21
 
35
22
  private
36
23
 
24
+ def client
25
+ @client ||= Clients::Whois.new(timeout:)
26
+ end
27
+
37
28
  #
38
29
  # @param [Mihari::Models::Artifact] artifact
39
30
  #
@@ -53,85 +44,9 @@ module Mihari
53
44
  # @return [Mihari::Models::WhoisRecord, nil]
54
45
  #
55
46
  def memoized_lookup(domain)
56
- whois.lookup domain
47
+ client.lookup domain
57
48
  end
58
49
  memo_wise :memoized_lookup
59
-
60
- #
61
- # @return [::Whois::Client]
62
- #
63
- def whois
64
- @whois ||= lambda do
65
- return ::Whois::Client.new if timeout.nil?
66
-
67
- ::Whois::Client.new(timeout:)
68
- end.call
69
- end
70
-
71
- #
72
- # Get created_on
73
- #
74
- # @param [::Whois::Parser] parser
75
- #
76
- # @return [Date, nil]
77
- #
78
- def get_created_on(parser)
79
- parser.created_on
80
- rescue ::Whois::AttributeNotImplemented
81
- nil
82
- end
83
-
84
- #
85
- # Get updated_on
86
- #
87
- # @param [::Whois::Parser] parser
88
- #
89
- # @return [Date, nil]
90
- #
91
- def get_updated_on(parser)
92
- parser.updated_on
93
- rescue ::Whois::AttributeNotImplemented
94
- nil
95
- end
96
-
97
- #
98
- # Get expires_on
99
- #
100
- # @param [::Whois::Parser] parser
101
- #
102
- # @return [Date, nil]
103
- #
104
- def get_expires_on(parser)
105
- parser.expires_on
106
- rescue ::Whois::AttributeNotImplemented
107
- nil
108
- end
109
-
110
- #
111
- # Get registrar
112
- #
113
- # @param [::Whois::Parser] parser
114
- #
115
- # @return [Hash, nil]
116
- #
117
- def get_registrar(parser)
118
- parser.registrar&.to_h
119
- rescue ::Whois::AttributeNotImplemented
120
- nil
121
- end
122
-
123
- #
124
- # Get contacts
125
- #
126
- # @param [::Whois::Parser] parser
127
- #
128
- # @return [Array<Hash>, nil]
129
- #
130
- def get_contacts(parser)
131
- parser.contacts.map(&:to_h)
132
- rescue ::Whois::AttributeNotImplemented
133
- nil
134
- end
135
50
  end
136
51
  end
137
52
  end
@@ -29,6 +29,13 @@ module Mihari
29
29
  optional(:options).hash(EmitterOptions)
30
30
  end
31
31
 
32
+ Yeti = Dry::Schema.Params do
33
+ required(:emitter).value(Types::String.enum(*Mihari::Emitters::Yeti.keys))
34
+ optional(:url).filled(:string)
35
+ optional(:api_key).filled(:string)
36
+ optional(:options).hash(EmitterOptions)
37
+ end
38
+
32
39
  Slack = Dry::Schema.Params do
33
40
  required(:emitter).value(Types::String.enum(*Mihari::Emitters::Slack.keys))
34
41
  optional(:webhook_url).filled(:string)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mihari
4
- VERSION = "7.4.0"
4
+ VERSION = "7.5.0"
5
5
  end