aikido-zen 0.1.0.alpha4-arm64-darwin

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 (85) 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/libzen-v0.1.26.aarch64.dylib +0 -0
  22. data/lib/aikido/zen/middleware/check_allowed_addresses.rb +38 -0
  23. data/lib/aikido/zen/middleware/set_context.rb +26 -0
  24. data/lib/aikido/zen/middleware/throttler.rb +50 -0
  25. data/lib/aikido/zen/outbound_connection.rb +45 -0
  26. data/lib/aikido/zen/outbound_connection_monitor.rb +19 -0
  27. data/lib/aikido/zen/package.rb +22 -0
  28. data/lib/aikido/zen/payload.rb +48 -0
  29. data/lib/aikido/zen/rails_engine.rb +53 -0
  30. data/lib/aikido/zen/rate_limiter/breaker.rb +61 -0
  31. data/lib/aikido/zen/rate_limiter/bucket.rb +76 -0
  32. data/lib/aikido/zen/rate_limiter/result.rb +31 -0
  33. data/lib/aikido/zen/rate_limiter.rb +55 -0
  34. data/lib/aikido/zen/request/heuristic_router.rb +109 -0
  35. data/lib/aikido/zen/request/rails_router.rb +84 -0
  36. data/lib/aikido/zen/request/schema/auth_discovery.rb +86 -0
  37. data/lib/aikido/zen/request/schema/auth_schemas.rb +40 -0
  38. data/lib/aikido/zen/request/schema/builder.rb +125 -0
  39. data/lib/aikido/zen/request/schema/definition.rb +112 -0
  40. data/lib/aikido/zen/request/schema/empty_schema.rb +28 -0
  41. data/lib/aikido/zen/request/schema.rb +72 -0
  42. data/lib/aikido/zen/request.rb +97 -0
  43. data/lib/aikido/zen/route.rb +39 -0
  44. data/lib/aikido/zen/runtime_settings/endpoints.rb +49 -0
  45. data/lib/aikido/zen/runtime_settings/ip_set.rb +36 -0
  46. data/lib/aikido/zen/runtime_settings/protection_settings.rb +62 -0
  47. data/lib/aikido/zen/runtime_settings/rate_limit_settings.rb +47 -0
  48. data/lib/aikido/zen/runtime_settings.rb +70 -0
  49. data/lib/aikido/zen/scan.rb +75 -0
  50. data/lib/aikido/zen/scanners/sql_injection_scanner.rb +95 -0
  51. data/lib/aikido/zen/scanners/ssrf/dns_lookups.rb +27 -0
  52. data/lib/aikido/zen/scanners/ssrf/private_ip_checker.rb +85 -0
  53. data/lib/aikido/zen/scanners/ssrf_scanner.rb +251 -0
  54. data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +43 -0
  55. data/lib/aikido/zen/scanners.rb +5 -0
  56. data/lib/aikido/zen/sink.rb +108 -0
  57. data/lib/aikido/zen/sinks/async_http.rb +63 -0
  58. data/lib/aikido/zen/sinks/curb.rb +89 -0
  59. data/lib/aikido/zen/sinks/em_http.rb +71 -0
  60. data/lib/aikido/zen/sinks/excon.rb +103 -0
  61. data/lib/aikido/zen/sinks/http.rb +76 -0
  62. data/lib/aikido/zen/sinks/httpclient.rb +68 -0
  63. data/lib/aikido/zen/sinks/httpx.rb +61 -0
  64. data/lib/aikido/zen/sinks/mysql2.rb +21 -0
  65. data/lib/aikido/zen/sinks/net_http.rb +85 -0
  66. data/lib/aikido/zen/sinks/patron.rb +88 -0
  67. data/lib/aikido/zen/sinks/pg.rb +50 -0
  68. data/lib/aikido/zen/sinks/resolv.rb +41 -0
  69. data/lib/aikido/zen/sinks/socket.rb +51 -0
  70. data/lib/aikido/zen/sinks/sqlite3.rb +30 -0
  71. data/lib/aikido/zen/sinks/trilogy.rb +21 -0
  72. data/lib/aikido/zen/sinks/typhoeus.rb +78 -0
  73. data/lib/aikido/zen/sinks.rb +21 -0
  74. data/lib/aikido/zen/stats/routes.rb +53 -0
  75. data/lib/aikido/zen/stats/sink_stats.rb +95 -0
  76. data/lib/aikido/zen/stats/users.rb +26 -0
  77. data/lib/aikido/zen/stats.rb +171 -0
  78. data/lib/aikido/zen/synchronizable.rb +24 -0
  79. data/lib/aikido/zen/system_info.rb +84 -0
  80. data/lib/aikido/zen/version.rb +10 -0
  81. data/lib/aikido/zen.rb +138 -0
  82. data/lib/aikido-zen.rb +3 -0
  83. data/lib/aikido.rb +3 -0
  84. data/tasklib/libzen.rake +128 -0
  85. metadata +175 -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