mihari 7.4.0 → 7.5.0

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