aikido-zen 0.1.0.alpha4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -0
  3. data/.standard.yml +3 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/LICENSE +674 -0
  7. data/README.md +40 -0
  8. data/Rakefile +63 -0
  9. data/lib/aikido/zen/actor.rb +116 -0
  10. data/lib/aikido/zen/agent.rb +187 -0
  11. data/lib/aikido/zen/api_client.rb +132 -0
  12. data/lib/aikido/zen/attack.rb +138 -0
  13. data/lib/aikido/zen/capped_collections.rb +68 -0
  14. data/lib/aikido/zen/config.rb +229 -0
  15. data/lib/aikido/zen/context/rack_request.rb +24 -0
  16. data/lib/aikido/zen/context/rails_request.rb +42 -0
  17. data/lib/aikido/zen/context.rb +101 -0
  18. data/lib/aikido/zen/errors.rb +88 -0
  19. data/lib/aikido/zen/event.rb +66 -0
  20. data/lib/aikido/zen/internals.rb +64 -0
  21. data/lib/aikido/zen/middleware/check_allowed_addresses.rb +38 -0
  22. data/lib/aikido/zen/middleware/set_context.rb +26 -0
  23. data/lib/aikido/zen/middleware/throttler.rb +50 -0
  24. data/lib/aikido/zen/outbound_connection.rb +45 -0
  25. data/lib/aikido/zen/outbound_connection_monitor.rb +19 -0
  26. data/lib/aikido/zen/package.rb +22 -0
  27. data/lib/aikido/zen/payload.rb +48 -0
  28. data/lib/aikido/zen/rails_engine.rb +53 -0
  29. data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
  30. data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
  31. data/lib/aikido/zen/rate_limiter/result.rb +31 -0
  32. data/lib/aikido/zen/rate_limiter.rb +55 -0
  33. data/lib/aikido/zen/request/heuristic_router.rb +109 -0
  34. data/lib/aikido/zen/request/rails_router.rb +84 -0
  35. data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
  36. data/lib/aikido/zen/request/schema/auth_schemas.rb +40 -0
  37. data/lib/aikido/zen/request/schema/builder.rb +125 -0
  38. data/lib/aikido/zen/request/schema/definition.rb +112 -0
  39. data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
  40. data/lib/aikido/zen/request/schema.rb +72 -0
  41. data/lib/aikido/zen/request.rb +97 -0
  42. data/lib/aikido/zen/route.rb +39 -0
  43. data/lib/aikido/zen/runtime_settings/endpoints.rb +49 -0
  44. data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
  45. data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
  46. data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
  47. data/lib/aikido/zen/runtime_settings.rb +70 -0
  48. data/lib/aikido/zen/scan.rb +75 -0
  49. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +95 -0
  50. data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
  51. data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +85 -0
  52. data/lib/aikido/zen/scanners/ssrf_scanner.rb +251 -0
  53. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +43 -0
  54. data/lib/aikido/zen/scanners.rb +5 -0
  55. data/lib/aikido/zen/sink.rb +108 -0
  56. data/lib/aikido/zen/sinks/async_http.rb +63 -0
  57. data/lib/aikido/zen/sinks/curb.rb +89 -0
  58. data/lib/aikido/zen/sinks/em_http.rb +71 -0
  59. data/lib/aikido/zen/sinks/excon.rb +103 -0
  60. data/lib/aikido/zen/sinks/http.rb +76 -0
  61. data/lib/aikido/zen/sinks/httpclient.rb +68 -0
  62. data/lib/aikido/zen/sinks/httpx.rb +61 -0
  63. data/lib/aikido/zen/sinks/mysql2.rb +21 -0
  64. data/lib/aikido/zen/sinks/net_http.rb +85 -0
  65. data/lib/aikido/zen/sinks/patron.rb +88 -0
  66. data/lib/aikido/zen/sinks/pg.rb +50 -0
  67. data/lib/aikido/zen/sinks/resolv.rb +41 -0
  68. data/lib/aikido/zen/sinks/socket.rb +51 -0
  69. data/lib/aikido/zen/sinks/sqlite3.rb +30 -0
  70. data/lib/aikido/zen/sinks/trilogy.rb +21 -0
  71. data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
  72. data/lib/aikido/zen/sinks.rb +21 -0
  73. data/lib/aikido/zen/stats/routes.rb +53 -0
  74. data/lib/aikido/zen/stats/sink_stats.rb +95 -0
  75. data/lib/aikido/zen/stats/users.rb +26 -0
  76. data/lib/aikido/zen/stats.rb +171 -0
  77. data/lib/aikido/zen/synchronizable.rb +24 -0
  78. data/lib/aikido/zen/system_info.rb +84 -0
  79. data/lib/aikido/zen/version.rb +10 -0
  80. data/lib/aikido/zen.rb +138 -0
  81. data/lib/aikido-zen.rb +3 -0
  82. data/lib/aikido.rb +3 -0
  83. data/tasklib/libzen.rake +128 -0
  84. metadata +171 -0
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sink"
4
+
5
+ module Aikido::Zen
6
+ module Sinks
7
+ module Trilogy
8
+ SINK = Sinks.add("trilogy", scanners: [Scanners::SQLInjectionScanner])
9
+
10
+ module Extensions
11
+ def query(query, *)
12
+ SINK.scan(query: query, dialect: :mysql, operation: "query")
13
+
14
+ super
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ ::Trilogy.prepend(Aikido::Zen::Sinks::Trilogy::Extensions)
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sink"
4
+ require_relative "../outbound_connection_monitor"
5
+
6
+ module Aikido::Zen
7
+ module Sinks
8
+ module Typhoeus
9
+ SINK = Sinks.add("typhoeus", scanners: [
10
+ Aikido::Zen::Scanners::SSRFScanner,
11
+ Aikido::Zen::OutboundConnectionMonitor
12
+ ])
13
+
14
+ before_callback = ->(request) {
15
+ wrapped_request = Aikido::Zen::Scanners::SSRFScanner::Request.new(
16
+ verb: request.options[:method],
17
+ uri: URI(request.url),
18
+ headers: request.options[:headers]
19
+ )
20
+
21
+ # Store the request information so the DNS sinks can pick it up.
22
+ if (context = Aikido::Zen.current_context)
23
+ prev_request = context["ssrf.request"]
24
+ context["ssrf.request"] = wrapped_request
25
+ end
26
+
27
+ SINK.scan(
28
+ connection: Aikido::Zen::OutboundConnection.from_uri(URI(request.base_url)),
29
+ request: wrapped_request,
30
+ operation: "request"
31
+ )
32
+
33
+ request.on_headers do |response|
34
+ context["ssrf.request"] = prev_request if context
35
+
36
+ Aikido::Zen::Scanners::SSRFScanner.track_redirects(
37
+ request: wrapped_request,
38
+ response: Aikido::Zen::Scanners::SSRFScanner::Response.new(
39
+ status: response.code,
40
+ headers: response.headers.to_h
41
+ )
42
+ )
43
+ end
44
+
45
+ # When Typhoeus is configured with followlocation: true, the redirect
46
+ # following happens between the on_headers and the on_complete callback,
47
+ # so we need this one to detect if the request resulted in an automatic
48
+ # redirect that was followed.
49
+ request.on_complete do |response|
50
+ break if response.effective_url == request.url
51
+
52
+ last_effective_request = Aikido::Zen::Scanners::SSRFScanner::Request.new(
53
+ verb: request.options[:method],
54
+ uri: URI(response.effective_url),
55
+ headers: request.options[:headers]
56
+ )
57
+ context["ssrf.request"] = last_effective_request if context
58
+
59
+ # In this case, we can't actually stop the request from happening, but
60
+ # we can scan again (now that we know another request happened), to
61
+ # stop the response from being exposed to the user. This downgrades
62
+ # the SSRF into a blind SSRF, which is better than doing nothing.
63
+ SINK.scan(
64
+ connection: Aikido::Zen::OutboundConnection.from_uri(URI(response.effective_url)),
65
+ request: last_effective_request,
66
+ operation: "request"
67
+ )
68
+ ensure
69
+ context["ssrf.request"] = nil
70
+ end
71
+
72
+ true
73
+ }
74
+
75
+ ::Typhoeus.before.prepend(before_callback)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sink"
4
+
5
+ require_relative "sinks/socket"
6
+
7
+ require_relative "sinks/resolv" if defined?(::Resolv)
8
+ require_relative "sinks/net_http" if defined?(::Net::HTTP)
9
+ require_relative "sinks/http" if defined?(::HTTP)
10
+ require_relative "sinks/httpx" if defined?(::HTTPX)
11
+ require_relative "sinks/httpclient" if defined?(::HTTPClient)
12
+ require_relative "sinks/excon" if defined?(::Excon)
13
+ require_relative "sinks/curb" if defined?(::Curl)
14
+ require_relative "sinks/patron" if defined?(::Patron)
15
+ require_relative "sinks/typhoeus" if defined?(::Typhoeus)
16
+ require_relative "sinks/async_http" if defined?(::Async::HTTP)
17
+ require_relative "sinks/em_http" if defined?(::EventMachine::HttpRequest)
18
+ require_relative "sinks/mysql2" if defined?(::Mysql2)
19
+ require_relative "sinks/pg" if defined?(::PG)
20
+ require_relative "sinks/sqlite3" if defined?(::SQLite3)
21
+ require_relative "sinks/trilogy" if defined?(::Trilogy)
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../request/schema/empty_schema"
4
+
5
+ class Aikido::Zen::Stats
6
+ # @api private
7
+ #
8
+ # Keeps track of the visited routes.
9
+ class Routes
10
+ def initialize
11
+ @routes = Hash.new do |h, k|
12
+ h[k] = Record.new(0, Aikido::Zen::Request::Schema::EMPTY_SCHEMA)
13
+ end
14
+ end
15
+
16
+ # @param route [Aikido::Zen::Route, nil] tracks the visit, if given.
17
+ # @param schema [Aikido::Zen::Request::Schema, nil] the schema of the
18
+ # request, if the feature is enabled.
19
+ # @return [void]
20
+ def add(route, schema = nil)
21
+ @routes[route].add(schema) if route
22
+ end
23
+
24
+ # @!visibility private
25
+ def [](route)
26
+ @routes[route]
27
+ end
28
+
29
+ # @!visibility private
30
+ def empty?
31
+ @routes.empty?
32
+ end
33
+
34
+ def as_json
35
+ @routes.map do |route, record|
36
+ {
37
+ method: route.verb,
38
+ path: route.path,
39
+ hits: record.hits,
40
+ apispec: record.schema&.as_json
41
+ }.compact
42
+ end
43
+ end
44
+
45
+ # @!visibility private
46
+ Record = Struct.new(:hits, :schema) do
47
+ def add(new_schema = nil)
48
+ self.hits += 1
49
+ self.schema |= new_schema
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../capped_collections"
4
+
5
+ module Aikido::Zen
6
+ # @api private
7
+ #
8
+ # Tracks data specific to a single Sink.
9
+ class Stats::SinkStats
10
+ # @return [Integer] number of total calls to Sink#scan.
11
+ attr_accessor :scans
12
+
13
+ # @return [Integer] number of scans where our scanners raised an
14
+ # error that was handled.
15
+ attr_accessor :errors
16
+
17
+ # @return [Integer] number of scans where an attack was detected.
18
+ attr_accessor :attacks
19
+
20
+ # @return [Integer] number of scans where an attack was detected
21
+ # _and_ blocked by the Zen.
22
+ attr_accessor :blocked_attacks
23
+
24
+ # @return [Set<Float>] keeps the duration of individual scans. If
25
+ # this grows to match Config#max_performance_samples, the set is
26
+ # cleared and the data is aggregated into #compressed_timings.
27
+ attr_accessor :timings
28
+
29
+ # @return [Array<CompressedTiming>] list of aggregated stats.
30
+ attr_accessor :compressed_timings
31
+
32
+ def initialize(name, config)
33
+ @name = name
34
+ @config = config
35
+
36
+ @scans = 0
37
+ @errors = 0
38
+
39
+ @attacks = 0
40
+ @blocked_attacks = 0
41
+
42
+ @timings = Set.new
43
+ @compressed_timings = CappedSet.new(@config.max_compressed_stats)
44
+ end
45
+
46
+ def add_timing(duration)
47
+ compress_timings if @timings.size >= @config.max_performance_samples
48
+ @timings << duration
49
+ end
50
+
51
+ def compress_timings(at: Time.now.utc)
52
+ return if @timings.empty?
53
+
54
+ list = @timings.sort
55
+ @timings.clear
56
+
57
+ mean = list.sum / list.size
58
+ percentiles = percentiles(list, 50, 75, 90, 95, 99)
59
+
60
+ @compressed_timings << CompressedTiming.new(mean, percentiles, at)
61
+ end
62
+
63
+ def as_json
64
+ {
65
+ total: @scans,
66
+ interceptorThrewError: @errors,
67
+ withoutContext: 0,
68
+ attacksDetected: {
69
+ total: @attacks,
70
+ blocked: @blocked_attacks
71
+ },
72
+ compressedTimings: @compressed_timings.as_json
73
+ }
74
+ end
75
+
76
+ private def percentiles(sorted, *scores)
77
+ return {} if sorted.empty? || scores.empty?
78
+
79
+ scores.map { |p|
80
+ index = (sorted.size * (p / 100.0)).floor
81
+ [p, sorted.at(index)]
82
+ }.to_h
83
+ end
84
+
85
+ CompressedTiming = Struct.new(:mean, :percentiles, :compressed_at) do
86
+ def as_json
87
+ {
88
+ averageInMs: mean * 1000,
89
+ percentiles: percentiles.transform_values { |t| t * 1000 },
90
+ compressedAt: compressed_at.to_i * 1000
91
+ }
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../capped_collections"
4
+
5
+ class Aikido::Zen::Stats
6
+ # @api private
7
+ #
8
+ # Keeps track of the users that were seen by the app.
9
+ class Users < Aikido::Zen::CappedMap
10
+ def add(actor)
11
+ if key?(actor.id)
12
+ self[actor.id].update
13
+ else
14
+ self[actor.id] = actor
15
+ end
16
+ end
17
+
18
+ def each(&b)
19
+ each_value(&b)
20
+ end
21
+
22
+ def as_json
23
+ map(&:as_json)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+
5
+ require_relative "capped_collections"
6
+
7
+ module Aikido::Zen
8
+ # Tracks information about how the Aikido Agent is used in the app.
9
+ class Stats < Concurrent::Synchronization::LockableObject
10
+ # @!visibility private
11
+ attr_reader :started_at, :ended_at, :requests, :aborted_requests, :sinks
12
+
13
+ # @!visibility private
14
+ attr_writer :ended_at
15
+
16
+ def initialize(config = Aikido::Zen.config)
17
+ super()
18
+ @config = config
19
+ reset_state
20
+ end
21
+
22
+ # @return [Boolean]
23
+ def empty?
24
+ synchronize { @requests.zero? && @sinks.empty? }
25
+ end
26
+
27
+ # @return [Boolean]
28
+ def any?
29
+ !empty?
30
+ end
31
+
32
+ # Track the timestamp we start tracking this series of stats.
33
+ #
34
+ # @return [void]
35
+ def start(at = Time.now.utc)
36
+ synchronize { @started_at = at }
37
+ end
38
+
39
+ # Atomically copies the stats and resets them to their initial values, so
40
+ # you can start gathering a new set while being able to read the old ones
41
+ # without fear that a thread might modify them.
42
+ #
43
+ # @param at [Time] the time at which we're resetting, which is set as the
44
+ # ending time for the returned copy.
45
+ #
46
+ # @return [Aikido::Zen::Stats] a frozen copy of the object before
47
+ # resetting, so you can access the data there, with its +ended_at+
48
+ # set to the given timestamp.
49
+ def reset(at: Time.now.utc)
50
+ synchronize {
51
+ # Make sure the timing stats are compressed before copying, since we
52
+ # need these compressed when we serialize this for the API.
53
+ @sinks.each_value(&:compress_timings)
54
+
55
+ copy = clone
56
+ copy.ended_at = at
57
+
58
+ reset_state
59
+ start(at)
60
+
61
+ copy
62
+ }
63
+ end
64
+
65
+ # @param request [Aikido::Zen::Request]
66
+ # @return [void]
67
+ def add_request(request)
68
+ synchronize {
69
+ @requests += 1
70
+ @routes.add(request.route, request.schema)
71
+ }
72
+ self
73
+ end
74
+
75
+ # @param connection [Aikido::Zen::OutboundConnection]
76
+ # @return [void]
77
+ def add_outbound(connection)
78
+ synchronize { @outbound_connections << connection }
79
+ self
80
+ end
81
+
82
+ # @param [Aikido::Zen::Scan]
83
+ # @return [void]
84
+ def add_scan(scan)
85
+ synchronize {
86
+ stats = @sinks[scan.sink.name]
87
+ stats.scans += 1
88
+ stats.errors += 1 if scan.errors?
89
+ stats.add_timing(scan.duration)
90
+ }
91
+ self
92
+ end
93
+
94
+ # @param attack [Aikido::Zen::Attack]
95
+ # @param being_blocked [Boolean] whether the Agent blocked the
96
+ # request where this Attack happened or not.
97
+ #
98
+ # @return [void]
99
+ def add_attack(attack, being_blocked:)
100
+ synchronize {
101
+ stats = @sinks[attack.sink.name]
102
+ stats.attacks += 1
103
+ stats.blocked_attacks += 1 if being_blocked
104
+ }
105
+ self
106
+ end
107
+
108
+ # @param actor [Aikido::Zen::Actor]
109
+ # @return [void]
110
+ def add_user(actor)
111
+ synchronize { @users.add(actor) }
112
+ end
113
+
114
+ # @return [#as_json] the set of routes visited during this stats-gathering
115
+ # period.
116
+ def routes
117
+ synchronize { @routes }
118
+ end
119
+
120
+ # @return [Enumerable<#as_json>] the set of users that had an active session
121
+ # during this stats-gathering period.
122
+ def users
123
+ synchronize { @users }
124
+ end
125
+
126
+ # @return [#as_json] the set of connections to outbound hosts that were
127
+ # established during this stats-gathering period.
128
+ def outbound_connections
129
+ synchronize { @outbound_connections }
130
+ end
131
+
132
+ def as_json
133
+ synchronize {
134
+ total_attacks, total_blocked = aggregate_attacks_from_sinks
135
+ {
136
+ startedAt: @started_at.to_i * 1000,
137
+ endedAt: (@ended_at.to_i * 1000 if @ended_at),
138
+ sinks: @sinks.transform_values(&:as_json),
139
+ requests: {
140
+ total: @requests,
141
+ aborted: @aborted_requests,
142
+ attacksDetected: {
143
+ total: total_attacks,
144
+ blocked: total_blocked
145
+ }
146
+ }
147
+ }
148
+ }
149
+ end
150
+
151
+ private def reset_state
152
+ @sinks = Hash.new { |h, k| h[k] = SinkStats.new(k, @config) }
153
+ @started_at = @ended_at = nil
154
+ @requests = 0
155
+ @routes = Routes.new
156
+ @users = Users.new(@config.max_users_tracked)
157
+ @outbound_connections = CappedSet.new(@config.max_outbound_connections)
158
+ @aborted_requests = 0
159
+ end
160
+
161
+ private def aggregate_attacks_from_sinks
162
+ @sinks.each_value.reduce([0, 0]) { |(attacks, blocked), stats|
163
+ [attacks + stats.attacks, blocked + stats.blocked_attacks]
164
+ }
165
+ end
166
+ end
167
+ end
168
+
169
+ require_relative "stats/users"
170
+ require_relative "stats/routes"
171
+ require_relative "stats/sink_stats"
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido::Zen
4
+ # @!visibility private
5
+ #
6
+ # Provides the synchronization part of Concurrent's LockableObject, but allows
7
+ # objects to take keyword arguments as well.
8
+ #
9
+ # NOTE: This is meant to be prepennded.
10
+ module Synchronizable
11
+ def initialize(*, **)
12
+ @__lock__ = ::Mutex.new
13
+ super
14
+ end
15
+
16
+ def synchronize
17
+ if @__lock__.owned?
18
+ yield
19
+ else
20
+ @__lock__.synchronize { yield }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "rubygems"
5
+
6
+ require_relative "package"
7
+
8
+ module Aikido::Zen
9
+ # Provides information about the currently running Agent.
10
+ class SystemInfo
11
+ def initialize(config = Aikido::Zen.config)
12
+ @config = config
13
+ end
14
+
15
+ def attacks_block_requests?
16
+ !!@config.blocking_mode
17
+ end
18
+
19
+ def attacks_are_only_reported?
20
+ !attacks_block_requests?
21
+ end
22
+
23
+ def library_name
24
+ "firewall-ruby"
25
+ end
26
+
27
+ def library_version
28
+ VERSION
29
+ end
30
+
31
+ def platform_version
32
+ RUBY_VERSION
33
+ end
34
+
35
+ def hostname
36
+ @hostname ||= Socket.gethostname
37
+ end
38
+
39
+ # @return [Array<Aikido::Zen::Package>] a list of loaded rubygems that are
40
+ # supported by Aikido (i.e. we have a Sink that scans the package for
41
+ # vulnerabilities and protects you).
42
+ def packages
43
+ @packages ||= Gem.loaded_specs
44
+ .map { |_, spec| Package.new(spec.name, spec.version) }
45
+ .select(&:supported?)
46
+ end
47
+
48
+ # @return [String] the first non-loopback IPv4 address that we can use
49
+ # to identify this host. If the machine is solely identified by IPv6
50
+ # addresses, then this will instead return an IPv6 address.
51
+ def ip_address
52
+ @ip_address ||= Socket.ip_address_list
53
+ .reject { |ip| ip.ipv4_loopback? || ip.ipv6_loopback? || ip.unix? }
54
+ .min_by { |ip| ip.ipv4? ? 0 : 1 }
55
+ .ip_address
56
+ end
57
+
58
+ def os_name
59
+ Gem::Platform.local.os
60
+ end
61
+
62
+ def os_version
63
+ Gem::Platform.local.version
64
+ end
65
+
66
+ def as_json
67
+ {
68
+ dryMode: attacks_are_only_reported?,
69
+ library: library_name,
70
+ version: library_version,
71
+ hostname: hostname,
72
+ ipAddress: ip_address,
73
+ platform: {version: platform_version},
74
+ os: {name: os_name, version: os_version},
75
+ packages: packages.reduce({}) { |all, package| all.update(package.as_json) },
76
+ incompatiblePackages: {},
77
+ stack: [],
78
+ serverless: false,
79
+ nodeEnv: "",
80
+ preventedPrototypePollution: false
81
+ }
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aikido
4
+ module Zen
5
+ VERSION = "0.1.0.alpha4"
6
+
7
+ # The version of libzen_internals that we build against.
8
+ LIBZEN_VERSION = "0.1.26"
9
+ end
10
+ end