aikido-zen 1.0.1.beta.4-arm64-linux → 1.0.2-arm64-linux

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/.simplecov +6 -0
  3. data/README.md +2 -0
  4. data/benchmarks/README.md +0 -1
  5. data/benchmarks/rails7.1_benchmark.js +1 -0
  6. data/benchmarks/rails7.1_sql_injection.js +52 -20
  7. data/docs/config.md +9 -1
  8. data/docs/proxy.md +10 -0
  9. data/docs/rails.md +55 -13
  10. data/docs/troubleshooting.md +62 -0
  11. data/lib/aikido/zen/actor.rb +34 -4
  12. data/lib/aikido/zen/agent/heartbeats_manager.rb +5 -5
  13. data/lib/aikido/zen/agent.rb +19 -17
  14. data/lib/aikido/zen/attack.rb +19 -9
  15. data/lib/aikido/zen/attack_wave/helpers.rb +457 -0
  16. data/lib/aikido/zen/attack_wave.rb +88 -0
  17. data/lib/aikido/zen/cache.rb +91 -0
  18. data/lib/aikido/zen/capped_collections.rb +22 -4
  19. data/lib/aikido/zen/collector/event.rb +238 -0
  20. data/lib/aikido/zen/collector/hosts.rb +16 -1
  21. data/lib/aikido/zen/collector/routes.rb +13 -8
  22. data/lib/aikido/zen/collector/stats.rb +33 -22
  23. data/lib/aikido/zen/collector/users.rb +5 -3
  24. data/lib/aikido/zen/collector.rb +107 -28
  25. data/lib/aikido/zen/config.rb +54 -21
  26. data/lib/aikido/zen/context/rack_request.rb +3 -0
  27. data/lib/aikido/zen/context/rails_request.rb +3 -0
  28. data/lib/aikido/zen/context.rb +42 -9
  29. data/lib/aikido/zen/detached_agent/agent.rb +28 -27
  30. data/lib/aikido/zen/detached_agent/front_object.rb +10 -6
  31. data/lib/aikido/zen/detached_agent/server.rb +63 -26
  32. data/lib/aikido/zen/event.rb +47 -2
  33. data/lib/aikido/zen/helpers.rb +24 -0
  34. data/lib/aikido/zen/internals.rb +23 -3
  35. data/lib/aikido/zen/libzen-v0.1.48-arm64-linux.so +0 -0
  36. data/lib/aikido/zen/middleware/{check_allowed_addresses.rb → allowed_address_checker.rb} +1 -1
  37. data/lib/aikido/zen/middleware/attack_wave_protector.rb +46 -0
  38. data/lib/aikido/zen/middleware/{set_context.rb → context_setter.rb} +1 -1
  39. data/lib/aikido/zen/middleware/fork_detector.rb +23 -0
  40. data/lib/aikido/zen/middleware/rack_throttler.rb +3 -1
  41. data/lib/aikido/zen/middleware/request_tracker.rb +9 -4
  42. data/lib/aikido/zen/outbound_connection.rb +18 -1
  43. data/lib/aikido/zen/payload.rb +1 -1
  44. data/lib/aikido/zen/rails_engine.rb +5 -8
  45. data/lib/aikido/zen/request/rails_router.rb +17 -2
  46. data/lib/aikido/zen/request.rb +21 -36
  47. data/lib/aikido/zen/route.rb +57 -0
  48. data/lib/aikido/zen/runtime_settings/endpoints.rb +37 -8
  49. data/lib/aikido/zen/runtime_settings.rb +6 -5
  50. data/lib/aikido/zen/scanners/path_traversal/helpers.rb +10 -7
  51. data/lib/aikido/zen/scanners/path_traversal_scanner.rb +5 -4
  52. data/lib/aikido/zen/scanners/shell_injection_scanner.rb +3 -2
  53. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +3 -2
  54. data/lib/aikido/zen/scanners/ssrf_scanner.rb +2 -1
  55. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +8 -2
  56. data/lib/aikido/zen/sink.rb +1 -1
  57. data/lib/aikido/zen/sinks/action_controller.rb +3 -1
  58. data/lib/aikido/zen/sinks/async_http.rb +40 -42
  59. data/lib/aikido/zen/sinks/curb.rb +56 -58
  60. data/lib/aikido/zen/sinks/em_http.rb +27 -29
  61. data/lib/aikido/zen/sinks/excon.rb +62 -65
  62. data/lib/aikido/zen/sinks/file.rb +108 -71
  63. data/lib/aikido/zen/sinks/http.rb +26 -28
  64. data/lib/aikido/zen/sinks/httpclient.rb +27 -29
  65. data/lib/aikido/zen/sinks/httpx.rb +27 -29
  66. data/lib/aikido/zen/sinks/kernel.rb +11 -12
  67. data/lib/aikido/zen/sinks/mysql2.rb +10 -12
  68. data/lib/aikido/zen/sinks/net_http.rb +25 -27
  69. data/lib/aikido/zen/sinks/patron.rb +56 -58
  70. data/lib/aikido/zen/sinks/pg.rb +23 -25
  71. data/lib/aikido/zen/sinks/resolv.rb +21 -21
  72. data/lib/aikido/zen/sinks/socket.rb +17 -12
  73. data/lib/aikido/zen/sinks/sqlite3.rb +18 -21
  74. data/lib/aikido/zen/sinks/trilogy.rb +10 -12
  75. data/lib/aikido/zen/sinks.rb +1 -4
  76. data/lib/aikido/zen/sinks_dsl.rb +39 -15
  77. data/lib/aikido/zen/system_info.rb +1 -5
  78. data/lib/aikido/zen/version.rb +2 -2
  79. data/lib/aikido/zen.rb +78 -16
  80. data/tasklib/bench.rake +1 -1
  81. data/tasklib/libzen.rake +1 -0
  82. metadata +15 -5
  83. data/lib/aikido/zen/libzen-v0.1.39-arm64-linux.so +0 -0
@@ -0,0 +1,457 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ module AttackWave
5
+ module Helpers
6
+ def self.web_scanner?(context)
7
+ return true if suspicious_request?(context)
8
+
9
+ return true if include_suspicious_payload?(context)
10
+
11
+ false
12
+ end
13
+
14
+ def self.suspicious_request?(context)
15
+ request = context.request
16
+
17
+ suspicious_method?(request.request_method) || suspicious_path?(request.path_info)
18
+ end
19
+
20
+ def self.suspicious_method?(method)
21
+ SUSPICIOUS_METHODS.include?(method.downcase)
22
+ end
23
+
24
+ def self.suspicious_path?(path)
25
+ path_parts = path.downcase.split("/")
26
+
27
+ file_name = path_parts.pop if path_parts.length > 0
28
+
29
+ if file_name
30
+ return true if SUSPICIOUS_FILE_NAMES.include?(file_name)
31
+
32
+ file_name_parts = file_name.split(".")
33
+
34
+ file_extension = file_name_parts.pop if file_name_parts.length > 1
35
+
36
+ return true if SUSPICIOUS_FILE_EXTENSIONS.include?(file_extension)
37
+ end
38
+
39
+ path_parts.any? do |directory_name|
40
+ SUSPICIOUS_DIRECTORY_NAMES.include?(directory_name)
41
+ end
42
+ end
43
+
44
+ def self.include_suspicious_payload?(context)
45
+ context.payloads.each do |payload|
46
+ next unless payload.source == :query
47
+
48
+ value = payload.value.downcase
49
+
50
+ length = value.length
51
+
52
+ next if length < 5 || length > 1_000
53
+
54
+ return true if SUSPICIOUS_SQL_KEYWORDS.any? do |keyword|
55
+ value.include?(keyword)
56
+ end
57
+ end
58
+
59
+ false
60
+ end
61
+
62
+ SUSPICIOUS_METHODS = [
63
+ "BADMETHOD",
64
+ "BADHTTPMETHOD",
65
+ "BADDATA",
66
+ "BADMTHD",
67
+ "BDMTHD"
68
+ ].map(&:downcase).freeze
69
+
70
+ SUSPICIOUS_DIRECTORY_NAMES = [
71
+ ".",
72
+ "..",
73
+ ".anydesk",
74
+ ".aptitude",
75
+ ".aws",
76
+ ".azure",
77
+ ".cache",
78
+ ".circleci",
79
+ ".config",
80
+ ".dbus",
81
+ ".docker",
82
+ ".drush",
83
+ ".TODO: gem",
84
+ ".git",
85
+ ".github",
86
+ ".gnupg",
87
+ ".gsutil",
88
+ ".hg",
89
+ ".idea",
90
+ ".java",
91
+ ".kube",
92
+ ".lftp",
93
+ ".minikube",
94
+ ".npm",
95
+ ".nvm",
96
+ ".pki",
97
+ ".snap",
98
+ ".ssh",
99
+ ".subversion",
100
+ ".svn",
101
+ ".tconn",
102
+ ".thunderbird",
103
+ ".tor",
104
+ ".vagrant.d",
105
+ ".vidalia",
106
+ ".vim",
107
+ ".vmware",
108
+ ".vscode",
109
+ "apache",
110
+ "apache2",
111
+ "grub",
112
+ "System32",
113
+ "tmp",
114
+ "xampp",
115
+ "cgi-bin",
116
+ "%systemroot%"
117
+ ].map(&:downcase).freeze
118
+
119
+ SUSPICIOUS_FILE_NAMES = [
120
+ ".addressbook",
121
+ ".atom",
122
+ ".bashrc",
123
+ ".boto",
124
+ ".config",
125
+ ".config.json",
126
+ ".config.xml",
127
+ ".config.yaml",
128
+ ".config.yml",
129
+ ".envrc",
130
+ ".eslintignore",
131
+ ".fbcindex",
132
+ ".forward",
133
+ ".gitattributes",
134
+ ".gitconfig",
135
+ ".gitignore",
136
+ ".gitkeep",
137
+ ".gitlab-ci.yaml",
138
+ ".gitlab-ci.yml",
139
+ ".gitmodules",
140
+ ".google_authenticator",
141
+ ".hgignore",
142
+ ".htaccess",
143
+ ".htpasswd",
144
+ ".htdigest",
145
+ ".ksh_history",
146
+ ".lesshst",
147
+ ".lhistory",
148
+ ".lighttpdpassword",
149
+ ".lldb-history",
150
+ ".lynx_cookies",
151
+ ".my.cnf",
152
+ ".mysql_history",
153
+ ".nano_history",
154
+ ".netrc",
155
+ ".node_repl_history",
156
+ ".npmrc",
157
+ ".nsconfig",
158
+ ".nsr",
159
+ ".password-store",
160
+ ".pearrc",
161
+ ".pgpass",
162
+ ".php_history",
163
+ ".pinerc",
164
+ ".proclog",
165
+ ".procmailrc",
166
+ ".profile",
167
+ ".psql_history",
168
+ ".python_history",
169
+ ".rediscli_history",
170
+ ".rhosts",
171
+ ".selected_editor",
172
+ ".sh_history",
173
+ ".sqlite_history",
174
+ ".svnignore",
175
+ ".tcshrc",
176
+ ".tmux.conf",
177
+ ".travis.yaml",
178
+ ".travis.yml",
179
+ ".viminfo",
180
+ ".vimrc",
181
+ ".www_acl",
182
+ ".wwwacl",
183
+ ".xauthority",
184
+ ".yarnrc",
185
+ ".zhistory",
186
+ ".zsh_history",
187
+ ".zshenv",
188
+ ".zshrc",
189
+ "Dockerfile",
190
+ "aws-key.yaml",
191
+ "aws-key.yml",
192
+ "aws.yaml",
193
+ "aws.yml",
194
+ "docker-compose.yaml",
195
+ "docker-compose.yml",
196
+ "npm-shrinkwrap.json",
197
+ "package-lock.json",
198
+ "package.json",
199
+ "phpinfo.php",
200
+ "wp-config.php",
201
+ "wp-config.php3",
202
+ "wp-config.php4",
203
+ "wp-config.php5",
204
+ "wp-config.phtml",
205
+ "composer.json",
206
+ "composer.lock",
207
+ "composer.phar",
208
+ "yarn.lock",
209
+ ".env.local",
210
+ ".env.development",
211
+ ".env.test",
212
+ ".env.production",
213
+ ".env.prod",
214
+ ".env.dev",
215
+ ".env.example",
216
+ "php.ini",
217
+ "wp-settings.php",
218
+ "config.asp",
219
+ "config_dev.asp",
220
+ "config-dev.asp",
221
+ "config.dev.asp",
222
+ "config_prod.asp",
223
+ "config-prod.asp",
224
+ "config.prod.asp",
225
+ "config.sample.asp",
226
+ "config-sample.asp",
227
+ "config_sample.asp",
228
+ "config_test.asp",
229
+ "config-test.asp",
230
+ "config.test.asp",
231
+ "config.ini",
232
+ "config_dev.ini",
233
+ "config-dev.ini",
234
+ "config.dev.ini",
235
+ "config_prod.ini",
236
+ "config-prod.ini",
237
+ "config.prod.ini",
238
+ "config.sample.ini",
239
+ "config-sample.ini",
240
+ "config_sample.ini",
241
+ "config_test.ini",
242
+ "config-test.ini",
243
+ "config.test.ini",
244
+ "config.json",
245
+ "config_dev.json",
246
+ "config-dev.json",
247
+ "config.dev.json",
248
+ "config_prod.json",
249
+ "config-prod.json",
250
+ "config.prod.json",
251
+ "config.sample.json",
252
+ "config-sample.json",
253
+ "config_sample.json",
254
+ "config_test.json",
255
+ "config-test.json",
256
+ "config.test.json",
257
+ "config.php",
258
+ "config_dev.php",
259
+ "config-dev.php",
260
+ "config.dev.php",
261
+ "config_prod.php",
262
+ "config-prod.php",
263
+ "config.prod.php",
264
+ "config.sample.php",
265
+ "config-sample.php",
266
+ "config_sample.php",
267
+ "config_test.php",
268
+ "config-test.php",
269
+ "config.test.php",
270
+ "config.pl",
271
+ "config_dev.pl",
272
+ "config-dev.pl",
273
+ "config.dev.pl",
274
+ "config_prod.pl",
275
+ "config-prod.pl",
276
+ "config.prod.pl",
277
+ "config.sample.pl",
278
+ "config-sample.pl",
279
+ "config_sample.pl",
280
+ "config_test.pl",
281
+ "config-test.pl",
282
+ "config.test.pl",
283
+ "config.py",
284
+ "config_dev.py",
285
+ "config-dev.py",
286
+ "config.dev.py",
287
+ "config_prod.py",
288
+ "config-prod.py",
289
+ "config.prod.py",
290
+ "config.sample.py",
291
+ "config-sample.py",
292
+ "config_sample.py",
293
+ "config_test.py",
294
+ "config-test.py",
295
+ "config.test.py",
296
+ "config.rb",
297
+ "config_dev.rb",
298
+ "config-dev.rb",
299
+ "config.dev.rb",
300
+ "config_prod.rb",
301
+ "config-prod.rb",
302
+ "config.prod.rb",
303
+ "config.sample.rb",
304
+ "config-sample.rb",
305
+ "config_sample.rb",
306
+ "config_test.rb",
307
+ "config-test.rb",
308
+ "config.test.rb",
309
+ "config.toml",
310
+ "config_dev.toml",
311
+ "config-dev.toml",
312
+ "config.dev.toml",
313
+ "config_prod.toml",
314
+ "config-prod.toml",
315
+ "config.prod.toml",
316
+ "config.sample.toml",
317
+ "config-sample.toml",
318
+ "config_sample.toml",
319
+ "config_test.toml",
320
+ "config-test.toml",
321
+ "config.test.toml",
322
+ "config.txt",
323
+ "config_dev.txt",
324
+ "config-dev.txt",
325
+ "config.dev.txt",
326
+ "config_prod.txt",
327
+ "config-prod.txt",
328
+ "config.prod.txt",
329
+ "config.sample.txt",
330
+ "config-sample.txt",
331
+ "config_sample.txt",
332
+ "config_test.txt",
333
+ "config-test.txt",
334
+ "config.test.txt",
335
+ "config.xml",
336
+ "config_dev.xml",
337
+ "config-dev.xml",
338
+ "config.dev.xml",
339
+ "config_prod.xml",
340
+ "config-prod.xml",
341
+ "config.prod.xml",
342
+ "config.sample.xml",
343
+ "config-sample.xml",
344
+ "config_sample.xml",
345
+ "config_test.xml",
346
+ "config-test.xml",
347
+ "config.test.xml",
348
+ "config.yaml",
349
+ "config_dev.yaml",
350
+ "config-dev.yaml",
351
+ "config.dev.yaml",
352
+ "config_prod.yaml",
353
+ "config-prod.yaml",
354
+ "config.prod.yaml",
355
+ "config.sample.yaml",
356
+ "config-sample.yaml",
357
+ "config_sample.yaml",
358
+ "config_test.yaml",
359
+ "config-test.yaml",
360
+ "config.test.yaml",
361
+ "config.yml",
362
+ "config_dev.yml",
363
+ "config-dev.yml",
364
+ "config.dev.yml",
365
+ "config_prod.yml",
366
+ "config-prod.yml",
367
+ "config.prod.yml",
368
+ "config.sample.yml",
369
+ "config-sample.yml",
370
+ "config_sample.yml",
371
+ "config_test.yml",
372
+ "config-test.yml",
373
+ "config.test.yml",
374
+ "boot.ini",
375
+ "gruntfile.js",
376
+ "localsettings.php",
377
+ "my.ini",
378
+ "npm-debug.log",
379
+ "parameters.yml",
380
+ "parameters.yaml",
381
+ "services.yml",
382
+ "services.yaml",
383
+ "web.config",
384
+ "webpack.config.js",
385
+ "config.old",
386
+ "config.inc.php",
387
+ "error.log",
388
+ "access.log",
389
+ ".DS_Store",
390
+ "passwd",
391
+ "win.ini",
392
+ "cmd.exe",
393
+ "my.cnf",
394
+ ".bash_history",
395
+ "docker-compose-dev.yml",
396
+ "docker-compose.override.yml",
397
+ "docker-compose.dev.yml",
398
+ "Cargo.lock",
399
+ "secrets.yml",
400
+ "secrets.yaml",
401
+ "docker-compose.staging.yml",
402
+ "docker-compose.production.yml",
403
+ "yaws-key.pem",
404
+ "mysql_config.ini",
405
+ "firewall.log",
406
+ "log4j.properties",
407
+ "serviceAccountCredentials.json",
408
+ "haproxy.cfg",
409
+ "service-account-credentials.json",
410
+ "vpn.log",
411
+ "system.log",
412
+ "webuser-auth.xml",
413
+ "fastcgi.conf",
414
+ "smb.conf",
415
+ "iis.log",
416
+ "pom.xml",
417
+ "openapi.json",
418
+ "vim_settings.xml",
419
+ "winscp.ini",
420
+ "ws_ftp.ini"
421
+ ].map(&:downcase).freeze
422
+
423
+ SUSPICIOUS_FILE_EXTENSIONS = [
424
+ "env",
425
+ "bak",
426
+ "sql",
427
+ "sqlite",
428
+ "sqlite3",
429
+ "db",
430
+ "old",
431
+ "save",
432
+ "orig",
433
+ "sqlitedb",
434
+ "sqlite3db"
435
+ ].map(&:downcase).freeze
436
+
437
+ SUSPICIOUS_SQL_KEYWORDS = [
438
+ "SELECT (CASE WHEN",
439
+ "SELECT COUNT(",
440
+ "SLEEP(",
441
+ "WAITFOR DELAY",
442
+ "SELECT LIKE(CHAR(",
443
+ "INFORMATION_SCHEMA.COLUMNS",
444
+ "INFORMATION_SCHEMA.TABLES",
445
+ "MD5(",
446
+ "DBMS_PIPE.RECEIVE_MESSAGE",
447
+ "SYSIBM.SYSTABLES",
448
+ "RANDOMBLOB(",
449
+ "SELECT * FROM",
450
+ "1'='1",
451
+ "PG_SLEEP(",
452
+ "UNION ALL SELECT",
453
+ "../"
454
+ ].map(&:downcase).freeze
455
+ end
456
+ end
457
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "cache"
4
+ require_relative "attack_wave/helpers"
5
+
6
+ module Aikido::Zen
7
+ module AttackWave
8
+ class Detector
9
+ def initialize(config: Aikido::Zen.config, clock: nil)
10
+ @config = config
11
+
12
+ @event_times = Cache.new(@config.attack_wave_max_cache_entries, ttl: @config.attack_wave_min_time_between_events, clock: clock)
13
+
14
+ @request_counts = Cache.new(@config.attack_wave_max_cache_entries, 0, ttl: @config.attack_wave_min_time_between_requests, clock: clock)
15
+ end
16
+
17
+ def attack_wave?(context)
18
+ client_ip = context.request.client_ip
19
+
20
+ return false unless client_ip
21
+
22
+ return false if @event_times[client_ip]
23
+
24
+ return false unless AttackWave::Helpers.web_scanner?(context)
25
+
26
+ request_count = @request_counts[client_ip] += 1
27
+
28
+ return false if request_count < @config.attack_wave_threshold
29
+
30
+ @event_times[client_ip] = Time.now.utc
31
+
32
+ true
33
+ end
34
+ end
35
+
36
+ class Request
37
+ # @return [String]
38
+ attr_reader :ip_address
39
+
40
+ # @return [String]
41
+ attr_reader :user_agent
42
+
43
+ # @return [String]
44
+ attr_reader :source
45
+
46
+ # @param ip_address [String]
47
+ # @param user_agent [String]
48
+ # @param source [String]
49
+ # @return [Aikido::Zen::AttackWave::Request]
50
+ def initialize(ip_address:, user_agent:, source:)
51
+ @ip_address = ip_address
52
+ @user_agent = user_agent
53
+ @source = source
54
+ end
55
+
56
+ def as_json
57
+ {
58
+ ipAddress: @ip_address,
59
+ userAgent: @user_agent,
60
+ source: @source
61
+ }.compact
62
+ end
63
+ end
64
+
65
+ class Attack
66
+ # @return [Hash<String, String>]
67
+ attr_reader :metadata
68
+
69
+ # @return [Aikido::Zen::Actor]
70
+ attr_reader :user
71
+
72
+ # @param metadata [Hash<String, String>]
73
+ # @param metadata [Aikido::Zen::Actor]
74
+ # @return [Aikido::Zen::AttackWave::Attack]
75
+ def initialize(metadata:, user:)
76
+ @metadata = metadata
77
+ @user = user
78
+ end
79
+
80
+ def as_json
81
+ {
82
+ metadata: @metadata.as_json,
83
+ user: @user.as_json
84
+ }.compact
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ class Cache
5
+ extend Forwardable
6
+
7
+ # @api private
8
+ # Visible for testing.
9
+ def_delegators :@data,
10
+ :size, :empty?
11
+
12
+ def initialize(capacity, default_value = nil, ttl:, clock: nil)
13
+ @default_value = default_value
14
+ @ttl = ttl
15
+ @clock = clock
16
+
17
+ @data = CappedMap.new(capacity, mode: :lru)
18
+ end
19
+
20
+ def key?(key)
21
+ @data.key?(key) && !@data[key].expired?
22
+ end
23
+
24
+ # @param key [Object] the key
25
+ # @param value [Object] the value
26
+ # @return [Object] the value that the key was set to
27
+ def []=(key, value)
28
+ if key?(key)
29
+ entry = @data[key]
30
+ entry.refresh
31
+ entry.value = value
32
+ else
33
+ @data[key] = CacheEntry.new(value, ttl: @ttl, clock: @clock)
34
+ end
35
+ end
36
+
37
+ def [](key)
38
+ if key?(key)
39
+ @data[key].value
40
+ else
41
+ @default_value
42
+ end
43
+ end
44
+
45
+ def delete(key)
46
+ if key?(key)
47
+ @data.delete(key).value
48
+ else
49
+ @data.delete(key)
50
+ nil
51
+ end
52
+ end
53
+
54
+ # @api private
55
+ # Visible for testing.
56
+ def to_a
57
+ @data.map { |key, entry| [key, entry.value] }
58
+ end
59
+
60
+ # @api private
61
+ # Visible for testing.
62
+ def to_h
63
+ to_a.to_h
64
+ end
65
+ end
66
+
67
+ class CacheEntry
68
+ attr_accessor :value
69
+
70
+ DEFAULT_CLOCK = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) }
71
+
72
+ # @param value [Object] the value
73
+ # @param ttl [Integer] the time-to-live in milliseconds
74
+ # @return [Aikido::Zen::CacheEntry]
75
+ def initialize(value, ttl:, clock: nil)
76
+ @value = value
77
+ @ttl = ttl
78
+ @clock = clock || DEFAULT_CLOCK
79
+
80
+ refresh
81
+ end
82
+
83
+ def refresh
84
+ @expires = @clock.call + @ttl
85
+ end
86
+
87
+ def expired?
88
+ @clock.call >= @expires
89
+ end
90
+ end
91
+ end
@@ -18,8 +18,8 @@ module Aikido::Zen
18
18
  # @return [Integer]
19
19
  attr_reader :capacity
20
20
 
21
- def initialize(capacity)
22
- @data = CappedMap.new(capacity)
21
+ def initialize(capacity, mode: :fifo)
22
+ @data = CappedMap.new(capacity, mode: mode)
23
23
  end
24
24
 
25
25
  def <<(element)
@@ -47,16 +47,23 @@ module Aikido::Zen
47
47
  extend Forwardable
48
48
 
49
49
  def_delegators :@data,
50
- :[], :fetch, :delete, :key?,
50
+ :delete, :key?,
51
51
  :each, :each_key, :each_value,
52
52
  :size, :empty?, :to_hash
53
53
 
54
54
  # @return [Integer]
55
55
  attr_reader :capacity
56
56
 
57
- def initialize(capacity)
57
+ def initialize(capacity, mode: :fifo)
58
58
  raise ArgumentError, "cannot set capacity lower than 1: #{capacity}" if capacity < 1
59
+
60
+ unless [:fifo, :lru].include?(mode)
61
+ raise ArgumentError, "unsupported mode: #{mode}"
62
+ end
63
+
59
64
  @capacity = capacity
65
+ @mode = mode
66
+
60
67
  @data = {}
61
68
  end
62
69
 
@@ -64,5 +71,16 @@ module Aikido::Zen
64
71
  @data[key] = value
65
72
  @data.delete(@data.each_key.first) if @data.size > @capacity
66
73
  end
74
+
75
+ def [](key)
76
+ @data[key] = @data.delete(key) if @mode == :lru && key?(key)
77
+ @data[key]
78
+ end
79
+
80
+ def fetch(key, ...)
81
+ return self[key] if key?(key)
82
+
83
+ @data.fetch(key, ...)
84
+ end
67
85
  end
68
86
  end