mihari 0.17.5 → 1.0.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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop.yml +155 -0
  4. data/.travis.yml +1 -0
  5. data/Gemfile +2 -0
  6. data/README.md +30 -72
  7. data/config/pre_commit.yml +3 -0
  8. data/lib/mihari.rb +12 -8
  9. data/lib/mihari/alert_viewer.rb +6 -28
  10. data/lib/mihari/analyzers/base.rb +7 -19
  11. data/lib/mihari/analyzers/basic.rb +3 -1
  12. data/lib/mihari/analyzers/binaryedge.rb +2 -2
  13. data/lib/mihari/analyzers/censys.rb +2 -2
  14. data/lib/mihari/analyzers/circl.rb +2 -2
  15. data/lib/mihari/analyzers/onyphe.rb +3 -3
  16. data/lib/mihari/analyzers/passivetotal.rb +2 -2
  17. data/lib/mihari/analyzers/pulsedive.rb +2 -2
  18. data/lib/mihari/analyzers/securitytrails.rb +2 -2
  19. data/lib/mihari/analyzers/securitytrails_domain_feed.rb +2 -2
  20. data/lib/mihari/analyzers/shodan.rb +2 -2
  21. data/lib/mihari/analyzers/virustotal.rb +2 -2
  22. data/lib/mihari/analyzers/zoomeye.rb +2 -2
  23. data/lib/mihari/cli.rb +2 -2
  24. data/lib/mihari/config.rb +68 -2
  25. data/lib/mihari/configurable.rb +1 -1
  26. data/lib/mihari/database.rb +45 -0
  27. data/lib/mihari/emitters/base.rb +1 -1
  28. data/lib/mihari/emitters/misp.rb +8 -1
  29. data/lib/mihari/emitters/slack.rb +2 -2
  30. data/lib/mihari/emitters/sqlite.rb +29 -0
  31. data/lib/mihari/emitters/stdout.rb +2 -1
  32. data/lib/mihari/emitters/the_hive.rb +28 -14
  33. data/lib/mihari/models/alert.rb +11 -0
  34. data/lib/mihari/models/artifact.rb +27 -0
  35. data/lib/mihari/models/tag.rb +10 -0
  36. data/lib/mihari/models/tagging.rb +10 -0
  37. data/lib/mihari/notifiers/slack.rb +4 -4
  38. data/lib/mihari/serializers/alert.rb +12 -0
  39. data/lib/mihari/serializers/artifact.rb +9 -0
  40. data/lib/mihari/serializers/tag.rb +9 -0
  41. data/lib/mihari/status.rb +1 -1
  42. data/lib/mihari/type_checker.rb +1 -1
  43. data/lib/mihari/version.rb +1 -1
  44. data/mihari.gemspec +11 -5
  45. metadata +120 -31
  46. data/lib/mihari/artifact.rb +0 -36
  47. data/lib/mihari/cache.rb +0 -35
  48. data/lib/mihari/the_hive.rb +0 -42
  49. data/lib/mihari/the_hive/alert.rb +0 -25
  50. data/lib/mihari/the_hive/artifact.rb +0 -33
  51. data/lib/mihari/the_hive/base.rb +0 -14
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 004a0d8f2ddeda5059f6748657b786b9470a290eb4d593ff0bb0632ebb495d3d
4
- data.tar.gz: c4f51acdb2dc76e52a445e382ce3c15b215b29f9abd860ca1c05fe814b24bf96
3
+ metadata.gz: '099c3dece3c31a745979b619c361d3bcff09e67abb3214c32747a935d57be963'
4
+ data.tar.gz: 1f777f2ca61721716e32e85846817a5183d11063573813adec757919bb27f457
5
5
  SHA512:
6
- metadata.gz: f3d1b8959e726240a7257f999347ff2fc81b9bf359948dfc9f09a9edad66d76225f8a4e16b105d1bda2ee5675100e13705fdb448dc6986493234280f849c4637
7
- data.tar.gz: 1b6dd5276812e5ba6f6c7997cb921acd261dbcced5af3599253b3995ced1450a8a998e8bdd88155ebe44ce3e724fb71da6e59fb72c2c6ed12222f1e1efd07dcc
6
+ metadata.gz: b8e0babf78935a90d1d4d39493ed7db77d498697a1ea8d2bbdf3277745544a0f7a59de6dc444e93065f66c339b9c5208df408748194cb490ffe3901681a9aa0b
7
+ data.tar.gz: 0b430d270f34f1fae64f384b672a4270500407e725f0f8f99bba278b134d74196fda894b4ed425ed21d363583da93e40648dce77b9ff39a9b0da07c7f5a7a5d2
data/.gitignore CHANGED
@@ -54,3 +54,6 @@ Gemfile.lock
54
54
 
55
55
  # solargraph
56
56
  .solargraph.yml
57
+
58
+ # SQLite
59
+ *.db
@@ -0,0 +1,155 @@
1
+ # Relaxed.Ruby.Style
2
+ ## Version 2.5
3
+
4
+ require:
5
+ - rubocop-performance
6
+
7
+ Style/Alias:
8
+ Enabled: false
9
+ StyleGuide: https://relaxed.ruby.style/#stylealias
10
+
11
+ Style/AsciiComments:
12
+ Enabled: false
13
+ StyleGuide: https://relaxed.ruby.style/#styleasciicomments
14
+
15
+ Style/BeginBlock:
16
+ Enabled: false
17
+ StyleGuide: https://relaxed.ruby.style/#stylebeginblock
18
+
19
+ Style/BlockDelimiters:
20
+ Enabled: false
21
+ StyleGuide: https://relaxed.ruby.style/#styleblockdelimiters
22
+
23
+ Style/CommentAnnotation:
24
+ Enabled: false
25
+ StyleGuide: https://relaxed.ruby.style/#stylecommentannotation
26
+
27
+ Style/Documentation:
28
+ Enabled: false
29
+ StyleGuide: https://relaxed.ruby.style/#styledocumentation
30
+
31
+ Layout/DotPosition:
32
+ Enabled: false
33
+ StyleGuide: https://relaxed.ruby.style/#layoutdotposition
34
+
35
+ Style/DoubleNegation:
36
+ Enabled: false
37
+ StyleGuide: https://relaxed.ruby.style/#styledoublenegation
38
+
39
+ Style/EndBlock:
40
+ Enabled: false
41
+ StyleGuide: https://relaxed.ruby.style/#styleendblock
42
+
43
+ Style/FormatString:
44
+ Enabled: false
45
+ StyleGuide: https://relaxed.ruby.style/#styleformatstring
46
+
47
+ Style/IfUnlessModifier:
48
+ Enabled: false
49
+ StyleGuide: https://relaxed.ruby.style/#styleifunlessmodifier
50
+
51
+ Style/Lambda:
52
+ Enabled: false
53
+ StyleGuide: https://relaxed.ruby.style/#stylelambda
54
+
55
+ Style/ModuleFunction:
56
+ Enabled: false
57
+ StyleGuide: https://relaxed.ruby.style/#stylemodulefunction
58
+
59
+ Style/MultilineBlockChain:
60
+ Enabled: false
61
+ StyleGuide: https://relaxed.ruby.style/#stylemultilineblockchain
62
+
63
+ Style/NegatedIf:
64
+ Enabled: false
65
+ StyleGuide: https://relaxed.ruby.style/#stylenegatedif
66
+
67
+ Style/NegatedWhile:
68
+ Enabled: false
69
+ StyleGuide: https://relaxed.ruby.style/#stylenegatedwhile
70
+
71
+ Style/NumericPredicate:
72
+ Enabled: false
73
+ StyleGuide: https://relaxed.ruby.style/#stylenumericpredicate
74
+
75
+ Style/ParallelAssignment:
76
+ Enabled: false
77
+ StyleGuide: https://relaxed.ruby.style/#styleparallelassignment
78
+
79
+ Style/PercentLiteralDelimiters:
80
+ Enabled: false
81
+ StyleGuide: https://relaxed.ruby.style/#stylepercentliteraldelimiters
82
+
83
+ Style/PerlBackrefs:
84
+ Enabled: false
85
+ StyleGuide: https://relaxed.ruby.style/#styleperlbackrefs
86
+
87
+ Style/Semicolon:
88
+ Enabled: false
89
+ StyleGuide: https://relaxed.ruby.style/#stylesemicolon
90
+
91
+ Style/SignalException:
92
+ Enabled: false
93
+ StyleGuide: https://relaxed.ruby.style/#stylesignalexception
94
+
95
+ Style/SingleLineBlockParams:
96
+ Enabled: false
97
+ StyleGuide: https://relaxed.ruby.style/#stylesinglelineblockparams
98
+
99
+ Style/SingleLineMethods:
100
+ Enabled: false
101
+ StyleGuide: https://relaxed.ruby.style/#stylesinglelinemethods
102
+
103
+ Layout/SpaceBeforeBlockBraces:
104
+ Enabled: false
105
+ StyleGuide: https://relaxed.ruby.style/#layoutspacebeforeblockbraces
106
+
107
+ Layout/SpaceInsideParens:
108
+ Enabled: false
109
+ StyleGuide: https://relaxed.ruby.style/#layoutspaceinsideparens
110
+
111
+ Style/SpecialGlobalVars:
112
+ Enabled: false
113
+ StyleGuide: https://relaxed.ruby.style/#stylespecialglobalvars
114
+
115
+ Style/StringLiterals:
116
+ Enabled: false
117
+ StyleGuide: https://relaxed.ruby.style/#stylestringliterals
118
+
119
+ Style/TrailingCommaInArguments:
120
+ Enabled: false
121
+ StyleGuide: https://relaxed.ruby.style/#styletrailingcommainarguments
122
+
123
+ Style/TrailingCommaInArrayLiteral:
124
+ Enabled: false
125
+ StyleGuide: https://relaxed.ruby.style/#styletrailingcommainarrayliteral
126
+
127
+ Style/TrailingCommaInHashLiteral:
128
+ Enabled: false
129
+ StyleGuide: https://relaxed.ruby.style/#styletrailingcommainhashliteral
130
+
131
+ Style/SymbolArray:
132
+ Enabled: false
133
+ StyleGuide: http://relaxed.ruby.style/#stylesymbolarray
134
+
135
+ Style/WhileUntilModifier:
136
+ Enabled: false
137
+ StyleGuide: https://relaxed.ruby.style/#stylewhileuntilmodifier
138
+
139
+ Style/WordArray:
140
+ Enabled: false
141
+ StyleGuide: https://relaxed.ruby.style/#stylewordarray
142
+
143
+ Lint/AmbiguousRegexpLiteral:
144
+ Enabled: false
145
+ StyleGuide: https://relaxed.ruby.style/#lintambiguousregexpliteral
146
+
147
+ Lint/AssignmentInCondition:
148
+ Enabled: false
149
+ StyleGuide: https://relaxed.ruby.style/#lintassignmentincondition
150
+
151
+ Layout/LineLength:
152
+ Enabled: false
153
+
154
+ Metrics:
155
+ Enabled: false
@@ -4,4 +4,5 @@ language: ruby
4
4
  cache: bundler
5
5
  rvm:
6
6
  - 2.6
7
+ - 2.7
7
8
  before_install: gem install bundler -v 2.1
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
 
3
5
  # Specify your gem's dependencies in mihari.gemspec
data/README.md CHANGED
@@ -10,19 +10,15 @@ Mihari is a helper to run queries & manage results continuously. Mihari can be u
10
10
 
11
11
  ## How it works
12
12
 
13
- - Mihari makes a query against Shodan, Censys, VirusTotal, SecurityTrails, etc. and extracts artifacts from the results.
14
- - Mihari checks whether [TheHive](https://thehive-project.org/) contains the artifacts or not.
13
+ - Mihari makes a query against Shodan, Censys, VirusTotal, SecurityTrails, etc. and extracts artifacts (IP addresses, domains, URLs and hashes) from the results.
14
+ - Mihari checks whether a DB (SQLite3) contains the artifacts or not.
15
15
  - If it doesn't contain the artifacts:
16
- - Mihari creates an alert on TheHive.
16
+ - Mihari creates an alert on TheHive. (Optional)
17
17
  - Mihari sends a notification to Slack. (Optional)
18
18
  - Mihari creates an event on MISP. (Optional)
19
19
 
20
20
  ![img](https://github.com/ninoseki/mihari/raw/master/screenshots/eyecatch.png)
21
21
 
22
- Check this blog post for more details: [Continuous C2 hunting with Censys, Shodan, Onyphe and TheHive](https://hackmd.io/s/SkUaSrqoE).
23
-
24
- You can use mihari without TheHive but note that mihari depends on TheHive to manage artifacts. It means mihari might make duplications when without TheHive.
25
-
26
22
  ### Screenshots
27
23
 
28
24
  - TheHive alert example
@@ -156,49 +152,13 @@ mihari http_hash --html /tmp/index.html
156
152
 
157
153
  ```bash
158
154
  # Censys lookup for PANDA C2
159
- $ mihari censys '("PANDA" AND "SMAdmin" AND "layui")' --title "PANDA C2"
160
- {
161
- "title": "PANDA C2",
162
- "description": "query = (\"PANDA\" AND \"SMAdmin\" AND \"layui\")",
163
- "artifacts": [
164
- "154.223.165.223",
165
- "154.194.2.31",
166
- "45.114.127.119",
167
- "..."
168
- ],
169
- "tags": []
170
- }
155
+ mihari censys '("PANDA" AND "SMAdmin" AND "layui")' --title "PANDA C2"
171
156
 
172
157
  # VirusTotal passive DNS lookup of a FAKESPY host
173
- $ mihari virustotal "jppost-hi.top" --title "FAKESPY host passive DNS results"
174
- {
175
- "title": "FAKESPY host passive DNS results",
176
- "description": "indicator = jppost-hi.top",
177
- "artifacts": [
178
- "185.22.152.28",
179
- "192.236.200.44",
180
- "193.148.69.12",
181
- "..."
182
- ],
183
- "tags": []
184
- }
158
+ mihari virustotal "jppost-hi.top" --title "FAKESPY passive DNS"
185
159
 
186
160
  # You can pass a "defanged" indicator as an input
187
- $ mihari virustotal "jppost-hi[.]top" --title "FAKESPY host passive DNS results"
188
-
189
- # SecurityTrails domain feed lookup for finding (possibly) Apple phishing websites
190
- $ mihari securitytrails_domain_feed "apple-" --type new
191
- {
192
- "title": "SecurityTrails domain feed lookup",
193
- "description": "Regexp = /apple-/",
194
- "artifacts": [
195
- "apple-sign.online",
196
- "apple-log-in.com",
197
- "apple-locator-id.info",
198
- "..."
199
- ],
200
- "tags": []
201
- }
161
+ mihari virustotal "jppost-hi[.]top" --title "FAKESPY passive DNS"
202
162
  ```
203
163
 
204
164
  ### Import from JSON
@@ -229,28 +189,29 @@ The input is a JSON data should have `title`, `description` and `artifacts` key.
229
189
 
230
190
  Configuration can be done via environment variables or a YAML file.
231
191
 
232
- | Key | Desc. | Recommended or optional |
233
- |------------------------|--------------------------------|--------------------------------|
234
- | THEHIVE_API_ENDPOINT | TheHive URL | Recommended |
235
- | THEHIVE_API_KEY | TheHive API key | Recommended |
236
- | MISP_API_ENDPOINT | MISP URL | Optional |
237
- | MISP_API_KEY | MISP API key | Optional |
238
- | SLACK_WEBHOOK_URL | Slack Webhook URL | Optional |
239
- | SLACK_CHANNEL | Slack channel name | Optional (default: `#general`) |
240
- | BINARYEDGE_API_KEY | BinaryEdge API key | Optional |
241
- | CENSYS_ID | Censys API ID | Optional |
242
- | CENSYS_SECRET | Censys secret | Optional |
243
- | CIRCL_PASSIVE_PASSWORD | CIRCL passive DNS/SSL password | Optional |
244
- | CIRCL_PASSIVE_USERNAME | CIRCL passive DNS/SSL username | Optional |
245
- | ONYPHE_API_KEY | Onyphe API key | Optional |
246
- | PASSIVETOTAL_API_KEY | PassiveTotal API key | Optional |
247
- | PASSIVETOTAL_USERNAME | PassiveTotal username | Optional |
248
- | PULSEDIVE_API_KEY | Pulsedive API key | Optional |
249
- | SECURITYTRAILS_API_KEY | SecurityTrails API key | Optional |
250
- | SHODAN_API_KEY | Shodan API key | Optional |
251
- | VIRUSTOTAL_API_KEY | VirusTotal API key | Optional |
252
- | ZOOMEYE_USERNAMME | ZoomEye username | Optional |
253
- | ZOOMEYE_PASSWORD | ZoomEye password | Optional |
192
+ | Key | Description | Default |
193
+ |------------------------|--------------------------------|-------------|
194
+ | DATABASE | A path to the SQLite database | `mihari.db` |
195
+ | BINARYEDGE_API_KEY | BinaryEdge API key | |
196
+ | CENSYS_ID | Censys API ID | |
197
+ | CENSYS_SECRET | Censys secret | |
198
+ | CIRCL_PASSIVE_PASSWORD | CIRCL passive DNS/SSL password | |
199
+ | CIRCL_PASSIVE_USERNAME | CIRCL passive DNS/SSL username | |
200
+ | MISP_API_ENDPOINT | MISP URL | |
201
+ | MISP_API_KEY | MISP API key | |
202
+ | ONYPHE_API_KEY | Onyphe API key | |
203
+ | PASSIVETOTAL_API_KEY | PassiveTotal API key | |
204
+ | PASSIVETOTAL_USERNAME | PassiveTotal username | |
205
+ | PULSEDIVE_API_KEY | Pulsedive API key | |
206
+ | SECURITYTRAILS_API_KEY | SecurityTrails API key | |
207
+ | SHODAN_API_KEY | Shodan API key | |
208
+ | SLACK_CHANNEL | Slack channel name | `#general` |
209
+ | SLACK_WEBHOOK_URL | Slack Webhook URL | |
210
+ | THEHIVE_API_ENDPOINT | TheHive URL | |
211
+ | THEHIVE_API_KEY | TheHive API key | |
212
+ | VIRUSTOTAL_API_KEY | VirusTotal API key | |
213
+ | ZOOMEYE_PASSWORD | ZoomEye password | |
214
+ | ZOOMEYE_USERNAMME | ZoomEye username | |
254
215
 
255
216
  Instead of using environment variables, you can use a YAML file for configuration.
256
217
 
@@ -261,6 +222,7 @@ mihari virustotal 1.1.1.1 --config /path/to/yaml.yml
261
222
  The YAML file should be a YAML hash like below:
262
223
 
263
224
  ```yaml
225
+ database: /tmp/mihari.db
264
226
  thehive_api_endpoint: https://localhost
265
227
  thehive_api_key: foo
266
228
  virustotal_api_key: foo
@@ -314,10 +276,6 @@ example.run
314
276
 
315
277
  See `/examples` for more.
316
278
 
317
- ## Caching
318
-
319
- Mihari caches execution results in `/tmp/mihari` and the default cache duration is 7 days. If you want to clear the cache, please clear `/tmp/mihari`.
320
-
321
279
  ## Using it with Docker
322
280
 
323
281
  ```bash
@@ -0,0 +1,3 @@
1
+ ---
2
+ :checks_add:
3
+ - :rubocop
@@ -19,24 +19,27 @@ module Mihari
19
19
  end
20
20
 
21
21
  require "mihari/version"
22
-
23
22
  require "mihari/errors"
24
23
 
25
- require "mihari/artifact"
26
- require "mihari/cache"
27
24
  require "mihari/config"
25
+
26
+ require "mihari/database"
28
27
  require "mihari/type_checker"
29
28
 
29
+ require "mihari/models/alert"
30
+ require "mihari/models/artifact"
31
+ require "mihari/models/tag"
32
+ require "mihari/models/tagging"
33
+
34
+ require "mihari/serializers/alert"
35
+ require "mihari/serializers/artifact"
36
+ require "mihari/serializers/tag"
37
+
30
38
  require "mihari/html"
31
39
 
32
40
  require "mihari/configurable"
33
41
  require "mihari/retriable"
34
42
 
35
- require "mihari/the_hive/base"
36
- require "mihari/the_hive/alert"
37
- require "mihari/the_hive/artifact"
38
- require "mihari/the_hive"
39
-
40
43
  require "mihari/analyzers/base"
41
44
  require "mihari/analyzers/basic"
42
45
 
@@ -70,6 +73,7 @@ require "mihari/notifiers/exception_notifier"
70
73
  require "mihari/emitters/base"
71
74
  require "mihari/emitters/misp"
72
75
  require "mihari/emitters/slack"
76
+ require "mihari/emitters/sqlite"
73
77
  require "mihari/emitters/stdout"
74
78
  require "mihari/emitters/the_hive"
75
79
 
@@ -3,39 +3,17 @@
3
3
  module Mihari
4
4
  class AlertViewer
5
5
  attr_reader :limit
6
- attr_reader :the_hive
7
-
8
- ALERT_KEYS = %w(title description artifacts tags createdAt status).freeze
9
6
 
10
7
  def initialize(limit: 5)
11
- @limit = limit
12
- validate_limit
13
-
14
- @the_hive = TheHive.new
15
- raise Error, "Cannot connect to the TheHive instance" unless the_hive.valid?
8
+ @limit = limit.to_i
9
+ raise ArgumentError, "limit should be bigger than zero" unless @limit.positive?
16
10
  end
17
11
 
18
12
  def list
19
- range = limit == "all" ? "all" : "0-#{limit}"
20
- alerts = the_hive.alert.list(range: range)
21
- alerts.map { |alert| convert alert }
22
- end
23
-
24
- private
25
-
26
- def validate_limit
27
- return true if limit == "all"
28
-
29
- raise ArgumentError, "limit should be bigger than zero" unless limit.to_i.positive?
30
- end
31
-
32
- def convert(alert)
33
- attributes = alert.select { |k, _v| ALERT_KEYS.include? k }
34
- attributes["createdAt"] = Time.at(attributes["createdAt"] / 1000).to_s
35
- attributes["artifacts"] = (attributes.dig("artifacts") || []).map do |artifact|
36
- artifact.dig("data")
37
- end.sort
38
- attributes
13
+ alerts = Alert.order(id: :desc).limit(limit).includes(:tags, :artifacts)
14
+ alerts.map do |alert|
15
+ AlertSerializer.new(alert).as_json
16
+ end
39
17
  end
40
18
  end
41
19
  end
@@ -23,6 +23,10 @@ module Mihari
23
23
  raise NotImplementedError, "You must implement #{self.class}##{__method__}"
24
24
  end
25
25
 
26
+ def source
27
+ self.class.to_s.split("::").last
28
+ end
29
+
26
30
  # @return [Array<String>]
27
31
  def tags
28
32
  []
@@ -37,7 +41,7 @@ module Mihari
37
41
  end
38
42
 
39
43
  def run_emitter(emitter)
40
- emitter.run(title: title, description: description, artifacts: unique_artifacts, tags: tags)
44
+ emitter.run(title: title, description: description, artifacts: unique_artifacts, source: source, tags: tags)
41
45
  rescue StandardError => e
42
46
  puts "Emission by #{emitter.class} is failed: #{e}"
43
47
  end
@@ -48,32 +52,16 @@ module Mihari
48
52
 
49
53
  private
50
54
 
51
- def the_hive
52
- @the_hive ||= TheHive.new
53
- end
54
-
55
- def cache
56
- @cache ||= Cache.new
57
- end
58
-
59
55
  # @return [Array<Mihari::Artifact>]
60
56
  def normalized_artifacts
61
57
  @normalized_artifacts ||= artifacts.compact.uniq.sort.map do |artifact|
62
- artifact.is_a?(Artifact) ? artifact : Artifact.new(artifact)
58
+ artifact.is_a?(Artifact) ? artifact : Artifact.new(data: artifact)
63
59
  end.select(&:valid?)
64
60
  end
65
61
 
66
- def uncached_artifacts
67
- @uncached_artifacts ||= normalized_artifacts.reject do |artifact|
68
- cache.cached? artifact.data
69
- end
70
- end
71
-
72
62
  # @return [Array<Mihari::Artifact>]
73
63
  def unique_artifacts
74
- return uncached_artifacts unless the_hive.valid?
75
-
76
- @unique_artifacts ||= the_hive.artifact.find_non_existing_artifacts(uncached_artifacts)
64
+ @unique_artifacts ||= normalized_artifacts.select(&:unique?)
77
65
  end
78
66
 
79
67
  def set_unique_artifacts