mihari 7.4.0 → 7.6.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: aa05d5cca592eb276667bc95e7c7a88c12be39bb547ffb4f3bac810f9e5a72a6
4
+ data.tar.gz: 81a9a73171dbb4c5171893cb23cc47963fcfef38d441cfebd37005e6e6dc6202
5
5
  SHA512:
6
- metadata.gz: 6dff8b5b3bcd3098bb90e84f1d026325ca5a24d2cffb30761e2c243dfbc81bfef8093bd885ef7e9b07a2338ef2ca1cba10686fd7612a6261205c08f0b9258a15
7
- data.tar.gz: 6958e9d9e344b98c29209ce4d5501ded9b8584367cf5556c99e351913e0fe8800d829ccedb53704951a7b29eb5b73950e071ff2460e95ad067aff81dbef2c81d
6
+ metadata.gz: d6623c5441e14cf967987c2341349e9973922fdb18dc5d9c3473bcc8fbc7aaf813f9a48c8384331e49a4ada370ebd270d3cd68cbc7dcc6481f1bb886b0923109
7
+ data.tar.gz: 0d112b4740054b7afe79effff48016b8549bf6ef45b337ba43b44fba13f162f7cc8b5f2bfe653bfacf4bb356c2f1b32c7a5964be174d028b877372d642aedfea
data/Rakefile CHANGED
@@ -55,17 +55,21 @@ namespace :build do
55
55
 
56
56
  puts "Swagger doc is built in #{elapsed}s"
57
57
  end
58
+
59
+ desc "Build frontend assets"
60
+ task :frontend do
61
+ # Build frontend assets
62
+ sh "cd frontend && npm install && npm run docs && npm run build-only"
63
+ # Copy built assets into ./lib/web/public/
64
+ sh "rm -rf ./lib/mihari/web/public/"
65
+ sh "mkdir -p ./lib/mihari/web/public/"
66
+ sh "cp -r frontend/dist/* ./lib/mihari/web/public"
67
+ end
58
68
  end
59
69
 
60
70
  task :build do
61
71
  Rake::Task["build:swagger"].invoke
62
-
63
- # Build ReDocs docs & frontend assets
64
- sh "cd frontend && npm install && npm run docs && npm run build-only"
65
- # Copy built assets into ./lib/web/public/
66
- sh "rm -rf ./lib/mihari/web/public/"
67
- sh "mkdir -p ./lib/mihari/web/public/"
68
- sh "cp -r frontend/dist/* ./lib/mihari/web/public"
72
+ Rake::Task["build:frontend"].invoke
69
73
  end
70
74
 
71
75
  # require it later enables doing pre-build step (= build the frontend app)
@@ -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.6.0"
5
5
  end
@@ -52,7 +52,7 @@ module Mihari
52
52
  end
53
53
 
54
54
  desc "Delete an alert", {
55
- success: {code: 204, model: Entities::Message},
55
+ success: {code: 204},
56
56
  failure: [{code: 404, model: Entities::ErrorMessage}],
57
57
  summary: "Delete an alert"
58
58
  }
@@ -60,11 +60,9 @@ module Mihari
60
60
  requires :id, type: Integer
61
61
  end
62
62
  delete "/:id" do
63
- status 204
64
-
65
63
  id = params["id"].to_i
66
64
  result = Services::AlertDestroyer.result(id)
67
- return present({message: ""}, with: Entities::Message) if result.success?
65
+ return if result.success?
68
66
 
69
67
  case result.failure
70
68
  when ActiveRecord::RecordNotFound
@@ -87,7 +87,7 @@ module Mihari
87
87
  end
88
88
 
89
89
  desc "Delete an artifact", {
90
- success: {code: 204, model: Entities::Message},
90
+ success: {code: 204},
91
91
  failure: [{code: 404, model: Entities::ErrorMessage}],
92
92
  summary: "Delete an artifact"
93
93
  }
@@ -99,7 +99,7 @@ module Mihari
99
99
 
100
100
  id = params["id"].to_i
101
101
  result = Services::ArtifactDestroyer.result(id)
102
- return present({message: ""}, with: Entities::Message) if result.success?
102
+ return if result.success?
103
103
 
104
104
  case result.failure
105
105
  when ActiveRecord::RecordNotFound
@@ -15,12 +15,7 @@ module Mihari
15
15
  }
16
16
  get "/" do
17
17
  configs = Services::ConfigSearcher.call
18
- present(
19
- {
20
- results: configs
21
- },
22
- with: Entities::Configs
23
- )
18
+ present({results: configs}, with: Entities::Configs)
24
19
  end
25
20
  end
26
21
  end
@@ -167,7 +167,7 @@ module Mihari
167
167
  end
168
168
 
169
169
  desc "Delete a rule", {
170
- success: {code: 204, model: Entities::Message},
170
+ success: {code: 204},
171
171
  failure: [{code: 404, model: Entities::ErrorMessage}],
172
172
  summary: "Delete a rule"
173
173
  }
@@ -179,7 +179,7 @@ module Mihari
179
179
 
180
180
  id = params[:id].to_s
181
181
  result = Services::RuleDestroyer.result(id)
182
- return present({message: "ID:#{id} is deleted"}, with: Entities::Message) if result.success?
182
+ return if result.success?
183
183
 
184
184
  case result.failure
185
185
  when ActiveRecord::RecordNotFound
@@ -32,7 +32,7 @@ module Mihari
32
32
  end
33
33
 
34
34
  desc "Delete a tag", {
35
- success: {code: 204, model: Entities::Message},
35
+ success: {code: 204},
36
36
  failure: [{code: 404, model: Entities::ErrorMessage}],
37
37
  summary: "Delete a tag"
38
38
  }
@@ -44,7 +44,7 @@ module Mihari
44
44
 
45
45
  id = params[:id].to_i
46
46
  result = Services::TagDestroyer.result(id)
47
- return present({message: ""}, with: Entities::Message) if result.success?
47
+ return if result.success?
48
48
 
49
49
  case result.failure
50
50
  when ActiveRecord::RecordNotFound