Class: Aikido::Zen::Agent

Inherits:
Object
  • Object
show all
Defined in:
lib/aikido/zen/agent.rb

Overview

Handles the background processes that communicate with the Aikido servers, including managing the runtime settings that keep the app protected.

Defined Under Namespace

Classes: HeartbeatsManager

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config: Aikido::Zen.config, collector: Aikido::Zen.collector, worker: Aikido::Zen::Worker.new(config: config), api_client: Aikido::Zen::APIClient.new(config: config), api_stream: Aikido::Zen::APIStream.new(config: config)) ⇒ Agent

Returns a new instance of Agent.



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/aikido/zen/agent.rb', line 19

def initialize(
  config: Aikido::Zen.config,
  collector: Aikido::Zen.collector,
  worker: Aikido::Zen::Worker.new(config: config),
  api_client: Aikido::Zen::APIClient.new(config: config),
  api_stream: Aikido::Zen::APIStream.new(config: config)
)
  @config = config
  @collector = collector
  @worker = worker
  @api_client = api_client
  @api_stream = api_stream

  @started_at = nil

  @runtime_config_update_mutex = Mutex.new
  @runtime_firewall_lists_update_mutex = Mutex.new
end

Class Method Details

.start(**opts) ⇒ Aikido::Zen::Agent

Initialize and start an agent instance.

Returns:



15
16
17
# File 'lib/aikido/zen/agent.rb', line 15

def self.start(**opts)
  new(**opts).tap(&:start!)
end

Instance Method Details

#handle_attack(attack) ⇒ void

This method returns an undefined value.

Given an Attack, report it to the Aikido server, and/or block the request depending on configuration.

Parameters:

  • attack (Attack)

    a detected attack.

Raises:



137
138
139
140
141
142
143
144
145
146
147
# File 'lib/aikido/zen/agent.rb', line 137

def handle_attack(attack)
  attack.will_be_blocked! if Aikido::Zen.blocking_mode?

  @config.logger.error(
    format("Zen has %s a %s: %s", attack.blocked? ? "blocked" : "detected", attack.humanized_name, attack.as_json.to_json)
  )
  report(Events::Attack.new(attack: attack)) if @api_client.can_make_requests?

  @collector.track_attack(attack)
  raise attack if attack.blocked?
end

#poll_for_setting_updatesvoid

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Sets up the timer task that polls the Aikido Runtime API for updates to the runtime settings every minute.

See Also:



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/aikido/zen/agent.rb', line 199

def poll_for_setting_updates
  @worker.every(@config.polling_interval) do
    if @api_client.should_fetch_settings?
      if update_settings_from_runtime_config!(@api_client.fetch_runtime_config, reason: "after polling")
        updated_settings!
      # :nocov:
      else
        # empty
      end
      # :nocov:

      update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists, reason: "after polling")
    end
  end
end

#report(event) {|response| ... } ⇒ void

This method returns an undefined value.

Asynchronously reports an Event of any kind to the Aikido dashboard. If given a block, the API response will be passed to the block for handling.

Parameters:

Yield Parameters:

  • response (Object)

    the response from the reporting API in case of a successful request.



157
158
159
160
161
162
163
164
# File 'lib/aikido/zen/agent.rb', line 157

def report(event)
  @worker.perform do
    response = @api_client.report(event)
    yield response if response && block_given?
  rescue Aikido::Zen::APIError, Aikido::Zen::NetworkError => err
    @config.logger.error(err.message)
  end
end

#send_heartbeat(at: Time.now.utc) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Atomically flushes all the stats stored by the agent, and sends a heartbeat event. Scheduled to run automatically on a recurring schedule when reporting is enabled.

Parameters:

  • at (Time) (defaults to: Time.now.utc)

    the event time. Defaults to now.

See Also:



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/aikido/zen/agent.rb', line 175

def send_heartbeat(at: Time.now.utc)
  return unless @api_client.can_make_requests?

  heartbeat = @collector.flush
  report(heartbeat) do |response|
    if update_settings_from_runtime_config!(response, reason: "after heartbeat")
      updated_settings!

      update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists, reason: "after heartbeat")
    # :nocov:
    else
      # empty
    end
    # :nocov:
  end
end

#start!Object

Raises:

  • (Aikido::ZenError)


42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/aikido/zen/agent.rb', line 42

def start!
  @config.logger.info("Starting Aikido agent v#{Aikido::Zen::VERSION}")

  raise Aikido::ZenError, "Aikido Agent already started!" if started?
  @started_at = Time.now.utc
  @collector.start(at: @started_at)

  if Aikido::Zen.blocking_mode?
    @config.logger.info("Requests identified as attacks will be blocked")
  else
    @config.logger.warn("Non-blocking mode enabled! No requests will be blocked")
  end

  if @api_client.can_make_requests?
    @config.logger.info("API Token set! Reporting has been enabled")
  else
    @config.logger.warn("No API Token set! Reporting has been disabled")
    return
  end

  # Code coverage is disabled here because the block is only called when
  # the process exits.
  # :nocov:
  at_exit { stop! if started? }
  # :nocov:

  report(Events::Started.new(time: @started_at)) do |response|
    if update_settings_from_runtime_config!(response, reason: "after start")
      updated_settings!
    # :nocov:
    else
      # empty
    end
    # :nocov:
  rescue => err
    @config.logger.error(err.message)
  end

  begin
    update_settings_from_runtime_firewall_lists!(@api_client.fetch_runtime_firewall_lists, reason: "after start")
  rescue => err
    @config.logger.error(err.message)
  end

  if @config.realtime_settings_updates_enabled?
    @api_stream.handle("config-updated") do |event|
      @config.logger.debug("Received server-sent event: config-updated")
      settings_updated(event)
    end

    @api_stream.start!
  end

  poll_for_setting_updates

  @config.initial_heartbeat_delays.each do |heartbeat_delay|
    @worker.delay(heartbeat_delay) do
      send_heartbeat
      @config.logger.info("Executed initial heartbeat after #{heartbeat_delay} seconds")
    end
  end
end

#started?Boolean

Returns:

  • (Boolean)


38
39
40
# File 'lib/aikido/zen/agent.rb', line 38

def started?
  !!@started_at
end

#stop!void

This method returns an undefined value.

Clean up any ongoing threads, and reset the state. Called automatically when the process exits.



109
110
111
112
113
114
115
# File 'lib/aikido/zen/agent.rb', line 109

def stop!
  @config.logger.info("Stopping Aikido agent")
  @started_at = nil
  @worker.shutdown

  @api_stream.stop!
end

#updated_settings!void

This method returns an undefined value.

Respond to the runtime settings changing after being fetched from the Aikido servers.



121
122
123
124
125
126
127
# File 'lib/aikido/zen/agent.rb', line 121

def updated_settings!
  if !heartbeats.running?
    heartbeats.start { send_heartbeat }
  elsif heartbeats.stale_settings?
    heartbeats.restart { send_heartbeat }
  end
end