mihari 7.4.0 → 7.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.
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