aikido-zen 0.1.0.alpha4 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.simplecov +19 -0
  3. data/CHANGELOG.md +16 -0
  4. data/README.md +136 -23
  5. data/Rakefile +4 -0
  6. data/benchmarks/README.md +27 -0
  7. data/benchmarks/rails7.1_sql_injection.js +74 -0
  8. data/docs/banner.svg +203 -0
  9. data/docs/config.md +123 -0
  10. data/docs/rails.md +70 -0
  11. data/lib/aikido/zen/actor.rb +1 -1
  12. data/lib/aikido/zen/agent/heartbeats_manager.rb +66 -0
  13. data/lib/aikido/zen/agent.rb +100 -112
  14. data/lib/aikido/zen/collector/hosts.rb +15 -0
  15. data/lib/aikido/zen/collector/routes.rb +64 -0
  16. data/lib/aikido/zen/{stats → collector}/sink_stats.rb +1 -1
  17. data/lib/aikido/zen/collector/stats.rb +111 -0
  18. data/lib/aikido/zen/{stats → collector}/users.rb +6 -2
  19. data/lib/aikido/zen/collector.rb +117 -0
  20. data/lib/aikido/zen/config.rb +17 -11
  21. data/lib/aikido/zen/context.rb +8 -1
  22. data/lib/aikido/zen/errors.rb +3 -1
  23. data/lib/aikido/zen/event.rb +7 -4
  24. data/lib/aikido/zen/internals.rb +4 -0
  25. data/lib/aikido/zen/middleware/set_context.rb +4 -1
  26. data/lib/aikido/zen/rails_engine.rb +27 -18
  27. data/lib/aikido/zen/request/schema/builder.rb +0 -2
  28. data/lib/aikido/zen/request.rb +6 -0
  29. data/lib/aikido/zen/runtime_settings.rb +6 -11
  30. data/lib/aikido/zen/scanners/ssrf_scanner.rb +12 -6
  31. data/lib/aikido/zen/sinks/action_controller.rb +64 -0
  32. data/lib/aikido/zen/sinks/http.rb +1 -1
  33. data/lib/aikido/zen/sinks/pg.rb +13 -12
  34. data/lib/aikido/zen/sinks/typhoeus.rb +1 -1
  35. data/lib/aikido/zen/sinks.rb +1 -0
  36. data/lib/aikido/zen/version.rb +2 -2
  37. data/lib/aikido/zen/worker.rb +82 -0
  38. data/lib/aikido/zen.rb +55 -50
  39. data/tasklib/bench.rake +70 -0
  40. metadata +19 -8
  41. data/CODE_OF_CONDUCT.md +0 -132
  42. data/lib/aikido/zen/stats/routes.rb +0 -53
  43. data/lib/aikido/zen/stats.rb +0 -171
data/lib/aikido/zen.rb CHANGED
@@ -4,7 +4,9 @@ require_relative "zen/version"
4
4
  require_relative "zen/errors"
5
5
  require_relative "zen/actor"
6
6
  require_relative "zen/config"
7
+ require_relative "zen/collector"
7
8
  require_relative "zen/system_info"
9
+ require_relative "zen/worker"
8
10
  require_relative "zen/agent"
9
11
  require_relative "zen/api_client"
10
12
  require_relative "zen/context"
@@ -24,12 +26,24 @@ module Aikido
24
26
  @config ||= Config.new
25
27
  end
26
28
 
29
+ # @return [Aikido::Zen::RuntimeSettings] the firewall configuration sourced
30
+ # from your Aikido dashboard. This is periodically polled for updates.
31
+ def self.runtime_settings
32
+ @runtime_settings ||= RuntimeSettings.new
33
+ end
34
+
27
35
  # Gets information about the current system configuration, which is sent to
28
36
  # the server along with any events.
29
37
  def self.system_info
30
38
  @system_info ||= SystemInfo.new
31
39
  end
32
40
 
41
+ # Manages runtime metrics extracted from your app, which are uploaded to the
42
+ # Aikido servers if configured to do so.
43
+ def self.collector
44
+ @collector ||= Collector.new
45
+ end
46
+
33
47
  # Gets the current context object that holds all information about the
34
48
  # current request.
35
49
  #
@@ -47,24 +61,13 @@ module Aikido
47
61
  Thread.current[:_aikido_current_context_] = context
48
62
  end
49
63
 
50
- # Track statistics about the result of a Sink's scan, and report it as an
51
- # Attack if one is detected.
52
- #
53
- # @param scan [Aikido::Zen::Scan]
54
- # @return [void]
55
- # @raise [Aikido::Zen::UnderAttackError] if the scan detected an Attack
56
- # and blocking_mode is enabled.
57
- def self.track_scan(scan)
58
- agent.stats.add_scan(scan)
59
- agent.handle_attack(scan.attack) if scan.attack?
60
- end
61
-
62
64
  # Track statistics about an HTTP request the app is handling.
63
65
  #
64
- # @param context [Aikido::Zen::Request]
66
+ # @param request [Aikido::Zen::Request]
65
67
  # @return [void]
66
68
  def self.track_request(request)
67
- agent.stats.add_request(request)
69
+ autostart
70
+ collector.track_request(request)
68
71
  end
69
72
 
70
73
  # Tracks a network connection made to an external service.
@@ -72,7 +75,21 @@ module Aikido
72
75
  # @param connection [Aikido::Zen::OutboundConnection]
73
76
  # @return [void]
74
77
  def self.track_outbound(connection)
75
- agent.stats.add_outbound(connection)
78
+ autostart
79
+ collector.track_outbound(connection)
80
+ end
81
+
82
+ # Track statistics about the result of a Sink's scan, and report it as
83
+ # an Attack if one is detected.
84
+ #
85
+ # @param scan [Aikido::Zen::Scan]
86
+ # @return [void]
87
+ # @raise [Aikido::Zen::UnderAttackError] if the scan detected an Attack
88
+ # and blocking_mode is enabled.
89
+ def self.track_scan(scan)
90
+ autostart
91
+ collector.track_scan(scan)
92
+ agent.handle_attack(scan.attack) if scan.attack?
76
93
  end
77
94
 
78
95
  # Track the user making the current request.
@@ -80,43 +97,22 @@ module Aikido
80
97
  # @param (see Aikido::Zen.Actor)
81
98
  # @return [void]
82
99
  def self.track_user(user)
83
- actor = Aikido::Zen::Actor(user)
100
+ return if config.disabled?
84
101
 
85
- if actor
86
- agent.stats.add_user(actor)
102
+ if (actor = Aikido::Zen::Actor(user))
103
+ autostart
104
+ collector.track_user(actor)
105
+ current_context.request.actor = actor if current_context
87
106
  else
88
- id_attr, name_attr = config.user_attribute_mappings.values_at(:id, :name)
89
- config.logger.warn(format(<<~LOG, obj: user, id: id_attr, name: name_attr))
90
- Incompatible object sent to Aikido::Zen.track_user: %<obj>p
91
-
92
- The object must satisfy one of the following:
107
+ config.logger.warn(format(<<~LOG, obj: user))
108
+ Incompatible object sent to track_user: %<obj>p
93
109
 
94
- * Implement #to_aikido_actor
95
- * Implement #to_model and have %<id>p and %<name>p attributes
96
- * Be a Hash with :id (or "id") and, optionally, :name (or "name") keys
110
+ The object must either implement #to_aikido_actor, or be a Hash with
111
+ an :id (or "id") and, optionally, a :name (or "name") key.
97
112
  LOG
98
113
  end
99
114
  end
100
115
 
101
- # Starts the background threads that keep the agent running.
102
- #
103
- # @return [void]
104
- def self.initialize!
105
- @agent ||= Agent.new
106
- @agent.start!
107
- end
108
-
109
- # Stop any background threads.
110
- def self.stop!
111
- @agent&.stop!
112
- end
113
-
114
- # @return [Aikido::Zen::RuntimeSettings] the firewall configuration sourced
115
- # from your Aikido dashboard. This is periodically polled for updates.
116
- def self.runtime_settings
117
- @runtime_settings ||= RuntimeSettings.new
118
- end
119
-
120
116
  # Load all sinks matching libraries loaded into memory. This method should
121
117
  # be called after all other dependencies have been loaded into memory (i.e.
122
118
  # at the end of the initialization process).
@@ -128,11 +124,20 @@ module Aikido
128
124
  require_relative "zen/sinks"
129
125
  end
130
126
 
131
- private_class_method def self.agent
132
- # We shouldn't start collecting data before we even initialize the agent,
133
- # but might as well make sure we have a @agent going to report to.
134
- @agent or initialize!
135
- @agent
127
+ # @!visibility private
128
+ # Stop any background threads.
129
+ def self.stop!
130
+ agent&.stop!
131
+ end
132
+
133
+ # @!visibility private
134
+ # Starts the background agent if it has not been started yet.
135
+ def self.agent
136
+ @agent ||= Agent.start
137
+ end
138
+
139
+ class << self
140
+ alias_method :autostart, :agent
136
141
  end
137
142
  end
138
143
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "timeout"
5
+
6
+ SERVER_PIDS = {}
7
+
8
+ def stop_servers
9
+ SERVER_PIDS.each { |_, pid| Process.kill("TERM", pid) }
10
+ SERVER_PIDS.clear
11
+ end
12
+
13
+ def boot_server(dir, port:, env: {})
14
+ env["PORT"] = port.to_s
15
+
16
+ Dir.chdir(dir) do
17
+ SERVER_PIDS[port] = Process.spawn(
18
+ env,
19
+ "rails", "server", "--pid", "#{Dir.pwd}/tmp/pids/server.#{port}.pid",
20
+ out: "/dev/null"
21
+ )
22
+ rescue
23
+ SERVER_PIDS.delete(port)
24
+ end
25
+ end
26
+
27
+ def port_open?(port, timeout: 1)
28
+ Timeout.timeout(timeout) do
29
+ TCPSocket.new("127.0.0.1", port).close
30
+ true
31
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError
32
+ false
33
+ end
34
+ rescue Timeout::Error
35
+ false
36
+ end
37
+
38
+ def wait_for_servers
39
+ ports = SERVER_PIDS.keys
40
+
41
+ Timeout.timeout(10) do
42
+ ports.reject! { |port| port_open?(port) } while ports.any?
43
+ end
44
+ rescue Timeout::Error
45
+ raise "Could not reach ports: #{ports.join(", ")}"
46
+ end
47
+
48
+ Pathname.glob("sample_apps/*").select(&:directory?).each do |dir|
49
+ namespace :bench do
50
+ namespace dir.basename.to_s do
51
+ desc "Run benchmarks for the #{dir.basename} sample app"
52
+ task run: [:boot_protected_app, :boot_unprotected_app] do
53
+ wait_for_servers
54
+ Dir.chdir("benchmarks") { sh "k6 run #{dir.basename}.js" }
55
+ ensure
56
+ stop_servers
57
+ end
58
+
59
+ task :boot_protected_app do
60
+ boot_server(dir, port: 3001)
61
+ end
62
+
63
+ task :boot_unprotected_app do
64
+ boot_server(dir, port: 3002, env: {"AIKIDO_DISABLE" => "true"})
65
+ end
66
+ end
67
+
68
+ task default: "#{dir.basename}:run"
69
+ end
70
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aikido-zen
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.alpha4
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicolas Sanguinetti
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-10-24 00:00:00.000000000 Z
11
+ date: 2024-11-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -60,20 +60,32 @@ extensions: []
60
60
  extra_rdoc_files: []
61
61
  files:
62
62
  - ".ruby-version"
63
+ - ".simplecov"
63
64
  - ".standard.yml"
64
65
  - CHANGELOG.md
65
- - CODE_OF_CONDUCT.md
66
66
  - LICENSE
67
67
  - README.md
68
68
  - Rakefile
69
+ - benchmarks/README.md
70
+ - benchmarks/rails7.1_sql_injection.js
71
+ - docs/banner.svg
72
+ - docs/config.md
73
+ - docs/rails.md
69
74
  - lib/aikido-zen.rb
70
75
  - lib/aikido.rb
71
76
  - lib/aikido/zen.rb
72
77
  - lib/aikido/zen/actor.rb
73
78
  - lib/aikido/zen/agent.rb
79
+ - lib/aikido/zen/agent/heartbeats_manager.rb
74
80
  - lib/aikido/zen/api_client.rb
75
81
  - lib/aikido/zen/attack.rb
76
82
  - lib/aikido/zen/capped_collections.rb
83
+ - lib/aikido/zen/collector.rb
84
+ - lib/aikido/zen/collector/hosts.rb
85
+ - lib/aikido/zen/collector/routes.rb
86
+ - lib/aikido/zen/collector/sink_stats.rb
87
+ - lib/aikido/zen/collector/stats.rb
88
+ - lib/aikido/zen/collector/users.rb
77
89
  - lib/aikido/zen/config.rb
78
90
  - lib/aikido/zen/context.rb
79
91
  - lib/aikido/zen/context/rack_request.rb
@@ -117,6 +129,7 @@ files:
117
129
  - lib/aikido/zen/scanners/stored_ssrf_scanner.rb
118
130
  - lib/aikido/zen/sink.rb
119
131
  - lib/aikido/zen/sinks.rb
132
+ - lib/aikido/zen/sinks/action_controller.rb
120
133
  - lib/aikido/zen/sinks/async_http.rb
121
134
  - lib/aikido/zen/sinks/curb.rb
122
135
  - lib/aikido/zen/sinks/em_http.rb
@@ -133,13 +146,11 @@ files:
133
146
  - lib/aikido/zen/sinks/sqlite3.rb
134
147
  - lib/aikido/zen/sinks/trilogy.rb
135
148
  - lib/aikido/zen/sinks/typhoeus.rb
136
- - lib/aikido/zen/stats.rb
137
- - lib/aikido/zen/stats/routes.rb
138
- - lib/aikido/zen/stats/sink_stats.rb
139
- - lib/aikido/zen/stats/users.rb
140
149
  - lib/aikido/zen/synchronizable.rb
141
150
  - lib/aikido/zen/system_info.rb
142
151
  - lib/aikido/zen/version.rb
152
+ - lib/aikido/zen/worker.rb
153
+ - tasklib/bench.rake
143
154
  - tasklib/libzen.rake
144
155
  homepage: https://aikido.dev
145
156
  licenses:
@@ -163,7 +174,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
163
174
  - !ruby/object:Gem::Version
164
175
  version: '0'
165
176
  requirements: []
166
- rubygems_version: 3.5.16
177
+ rubygems_version: 3.5.22
167
178
  signing_key:
168
179
  specification_version: 4
169
180
  summary: Embedded Web Application Firewall that autonomously protects Ruby apps against
data/CODE_OF_CONDUCT.md DELETED
@@ -1,132 +0,0 @@
1
- # Contributor Covenant Code of Conduct
2
-
3
- ## Our Pledge
4
-
5
- We as members, contributors, and leaders pledge to make participation in our
6
- community a harassment-free experience for everyone, regardless of age, body
7
- size, visible or invisible disability, ethnicity, sex characteristics, gender
8
- identity and expression, level of experience, education, socio-economic status,
9
- nationality, personal appearance, race, caste, color, religion, or sexual
10
- identity and orientation.
11
-
12
- We pledge to act and interact in ways that contribute to an open, welcoming,
13
- diverse, inclusive, and healthy community.
14
-
15
- ## Our Standards
16
-
17
- Examples of behavior that contributes to a positive environment for our
18
- community include:
19
-
20
- * Demonstrating empathy and kindness toward other people
21
- * Being respectful of differing opinions, viewpoints, and experiences
22
- * Giving and gracefully accepting constructive feedback
23
- * Accepting responsibility and apologizing to those affected by our mistakes,
24
- and learning from the experience
25
- * Focusing on what is best not just for us as individuals, but for the overall
26
- community
27
-
28
- Examples of unacceptable behavior include:
29
-
30
- * The use of sexualized language or imagery, and sexual attention or advances of
31
- any kind
32
- * Trolling, insulting or derogatory comments, and personal or political attacks
33
- * Public or private harassment
34
- * Publishing others' private information, such as a physical or email address,
35
- without their explicit permission
36
- * Other conduct which could reasonably be considered inappropriate in a
37
- professional setting
38
-
39
- ## Enforcement Responsibilities
40
-
41
- Community leaders are responsible for clarifying and enforcing our standards of
42
- acceptable behavior and will take appropriate and fair corrective action in
43
- response to any behavior that they deem inappropriate, threatening, offensive,
44
- or harmful.
45
-
46
- Community leaders have the right and responsibility to remove, edit, or reject
47
- comments, commits, code, wiki edits, issues, and other contributions that are
48
- not aligned to this Code of Conduct, and will communicate reasons for moderation
49
- decisions when appropriate.
50
-
51
- ## Scope
52
-
53
- This Code of Conduct applies within all community spaces, and also applies when
54
- an individual is officially representing the community in public spaces.
55
- Examples of representing our community include using an official email address,
56
- posting via an official social media account, or acting as an appointed
57
- representative at an online or offline event.
58
-
59
- ## Enforcement
60
-
61
- Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
- reported to the community leaders responsible for enforcement at
63
- [hello@aikido.dev](mailto:hello@aikido.dev).
64
- All complaints will be reviewed and investigated promptly and fairly.
65
-
66
- All community leaders are obligated to respect the privacy and security of the
67
- reporter of any incident.
68
-
69
- ## Enforcement Guidelines
70
-
71
- Community leaders will follow these Community Impact Guidelines in determining
72
- the consequences for any action they deem in violation of this Code of Conduct:
73
-
74
- ### 1. Correction
75
-
76
- **Community Impact**: Use of inappropriate language or other behavior deemed
77
- unprofessional or unwelcome in the community.
78
-
79
- **Consequence**: A private, written warning from community leaders, providing
80
- clarity around the nature of the violation and an explanation of why the
81
- behavior was inappropriate. A public apology may be requested.
82
-
83
- ### 2. Warning
84
-
85
- **Community Impact**: A violation through a single incident or series of
86
- actions.
87
-
88
- **Consequence**: A warning with consequences for continued behavior. No
89
- interaction with the people involved, including unsolicited interaction with
90
- those enforcing the Code of Conduct, for a specified period of time. This
91
- includes avoiding interactions in community spaces as well as external channels
92
- like social media. Violating these terms may lead to a temporary or permanent
93
- ban.
94
-
95
- ### 3. Temporary Ban
96
-
97
- **Community Impact**: A serious violation of community standards, including
98
- sustained inappropriate behavior.
99
-
100
- **Consequence**: A temporary ban from any sort of interaction or public
101
- communication with the community for a specified period of time. No public or
102
- private interaction with the people involved, including unsolicited interaction
103
- with those enforcing the Code of Conduct, is allowed during this period.
104
- Violating these terms may lead to a permanent ban.
105
-
106
- ### 4. Permanent Ban
107
-
108
- **Community Impact**: Demonstrating a pattern of violation of community
109
- standards, including sustained inappropriate behavior, harassment of an
110
- individual, or aggression toward or disparagement of classes of individuals.
111
-
112
- **Consequence**: A permanent ban from any sort of public interaction within the
113
- community.
114
-
115
- ## Attribution
116
-
117
- This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
- version 2.1, available at
119
- [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
-
121
- Community Impact Guidelines were inspired by
122
- [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
-
124
- For answers to common questions about this code of conduct, see the FAQ at
125
- [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
- [https://www.contributor-covenant.org/translations][translations].
127
-
128
- [homepage]: https://www.contributor-covenant.org
129
- [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
- [Mozilla CoC]: https://github.com/mozilla/diversity
131
- [FAQ]: https://www.contributor-covenant.org/faq
132
- [translations]: https://www.contributor-covenant.org/translations
@@ -1,53 +0,0 @@
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
@@ -1,171 +0,0 @@
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"