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 +4 -4
- data/Rakefile +11 -7
- data/lib/mihari/clients/base.rb +3 -2
- data/lib/mihari/clients/whois.rb +118 -0
- data/lib/mihari/clients/yeti.rb +38 -0
- data/lib/mihari/config.rb +8 -0
- data/lib/mihari/data_type.rb +1 -3
- data/lib/mihari/emitters/yeti.rb +107 -0
- data/lib/mihari/enrichers/whois.rb +6 -91
- data/lib/mihari/schemas/emitter.rb +7 -0
- data/lib/mihari/version.rb +1 -1
- data/lib/mihari/web/endpoints/alerts.rb +2 -4
- data/lib/mihari/web/endpoints/artifacts.rb +2 -2
- data/lib/mihari/web/endpoints/configs.rb +1 -6
- data/lib/mihari/web/endpoints/rules.rb +2 -2
- data/lib/mihari/web/endpoints/tags.rb +2 -2
- data/lib/mihari/web/public/assets/index-CNoViC5p.css +1 -0
- data/lib/mihari/web/public/assets/index-ruBsf_QV.js +1783 -0
- data/lib/mihari/web/public/index.html +2 -2
- data/lib/mihari/web/public/redoc-static.html +26 -22
- data/lib/mihari.rb +3 -0
- data/mihari.gemspec +12 -12
- data/requirements.txt +1 -1
- metadata +31 -28
- data/lib/mihari/web/public/assets/index-DsMIBgVm.js +0 -1787
- data/lib/mihari/web/public/assets/index-qLffdzXi.css +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aa05d5cca592eb276667bc95e7c7a88c12be39bb547ffb4f3bac810f9e5a72a6
|
4
|
+
data.tar.gz: 81a9a73171dbb4c5171893cb23cc47963fcfef38d441cfebd37005e6e6dc6202
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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)
|
data/lib/mihari/clients/base.rb
CHANGED
@@ -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
|
|
data/lib/mihari/data_type.rb
CHANGED
@@ -26,9 +26,7 @@ module Mihari
|
|
26
26
|
|
27
27
|
# @return [Boolean]
|
28
28
|
def ip?
|
29
|
-
Try[IPAddr::InvalidAddressError]
|
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
|
-
|
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
|
-
|
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)
|
data/lib/mihari/version.rb
CHANGED
@@ -52,7 +52,7 @@ module Mihari
|
|
52
52
|
end
|
53
53
|
|
54
54
|
desc "Delete an alert", {
|
55
|
-
success: {code: 204
|
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
|
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
|
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
|
102
|
+
return if result.success?
|
103
103
|
|
104
104
|
case result.failure
|
105
105
|
when ActiveRecord::RecordNotFound
|
@@ -167,7 +167,7 @@ module Mihari
|
|
167
167
|
end
|
168
168
|
|
169
169
|
desc "Delete a rule", {
|
170
|
-
success: {code: 204
|
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
|
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
|
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
|
47
|
+
return if result.success?
|
48
48
|
|
49
49
|
case result.failure
|
50
50
|
when ActiveRecord::RecordNotFound
|