Skip to content

Commit

Permalink
Merge pull request #88 from AikidoSec/add-stats
Browse files Browse the repository at this point in the history
Store monitored sink stats
  • Loading branch information
willem-delbare authored Jan 8, 2025
2 parents 1796b67 + 177bb40 commit 6c4f485
Show file tree
Hide file tree
Showing 31 changed files with 351 additions and 27 deletions.
2 changes: 2 additions & 0 deletions lib/agent/aikido_types/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ type StatsDataType struct {
RequestsAborted int
Attacks int
AttacksBlocked int

MonitoredSinkStats map[string]MonitoredSinkStats
}

type RateLimitingConfig struct {
Expand Down
2 changes: 2 additions & 0 deletions lib/agent/cloud/cloud.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cloud

import (
. "main/aikido_types"
"main/globals"
"main/utils"
"time"
Expand All @@ -19,6 +20,7 @@ func Init() {
utils.StartPollingRoutine(ConfigPollingRoutineChannel, ConfigPollingTicker, CheckConfigUpdatedAt)

globals.StatsData.StartedAt = utils.GetTime()
globals.StatsData.MonitoredSinkStats = make(map[string]MonitoredSinkStats)
globals.MiddlewareInstalled = 0
}

Expand Down
3 changes: 2 additions & 1 deletion lib/agent/cloud/event_heartbeat.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func GetStatsAndClear() Stats {
defer globals.StatsData.StatsMutex.Unlock()

stats := Stats{
Sinks: make(map[string]MonitoredSinkStats),
Sinks: globals.StatsData.MonitoredSinkStats,
StartedAt: globals.StatsData.StartedAt,
EndedAt: utils.GetTime(),
Requests: Requests{
Expand All @@ -79,6 +79,7 @@ func GetStatsAndClear() Stats {
globals.StatsData.RequestsAborted = 0
globals.StatsData.Attacks = 0
globals.StatsData.AttacksBlocked = 0
globals.StatsData.MonitoredSinkStats = make(map[string]MonitoredSinkStats)

return stats
}
Expand Down
2 changes: 1 addition & 1 deletion lib/agent/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ toolchain go1.23.3

require (
github.com/stretchr/testify v1.9.0
google.golang.org/grpc v1.69.0
google.golang.org/grpc v1.69.2
google.golang.org/protobuf v1.35.1
)

Expand Down
2 changes: 2 additions & 0 deletions lib/agent/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
google.golang.org/grpc v1.69.0 h1:quSiOM1GJPmPH5XtU+BCoVXcDVJJAzNcoyfC2cCjGkI=
google.golang.org/grpc v1.69.0/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
Expand Down
38 changes: 38 additions & 0 deletions lib/agent/grpc/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,44 @@ func storeStats() {
globals.StatsData.Requests += 1
}

func storeAttackStats(req *protos.AttackDetected) {
globals.StatsData.StatsMutex.Lock()
defer globals.StatsData.StatsMutex.Unlock()

globals.StatsData.Attacks += 1
if req.GetAttack().GetBlocked() {
globals.StatsData.AttacksBlocked += 1
}
}

func getCompressedTiming(protoCompressedTiming *protos.CompressedTiming) CompressedTiming {
return CompressedTiming{
AverageInMS: protoCompressedTiming.GetAverageInMs(),
Percentiles: protoCompressedTiming.GetPercentiles(),
CompressedAt: utils.GetTime(),
}
}

func storeSinkStats(protoSinkStats *protos.MonitoredSinkStats) {
globals.StatsData.StatsMutex.Lock()
defer globals.StatsData.StatsMutex.Unlock()

sink := protoSinkStats.GetSink()
monitoredSinkStats, found := globals.StatsData.MonitoredSinkStats[sink]
if !found {
monitoredSinkStats = MonitoredSinkStats{}
}

monitoredSinkStats.AttacksDetected.Total += int(protoSinkStats.GetAttacksDetected())
monitoredSinkStats.AttacksDetected.Blocked += int(protoSinkStats.GetAttacksBlocked())
monitoredSinkStats.InterceptorThrewError += int(protoSinkStats.GetInterceptorThrewError())
monitoredSinkStats.WithoutContext += int(protoSinkStats.GetWithoutContext())
monitoredSinkStats.Total += int(protoSinkStats.GetTotal())
monitoredSinkStats.CompressedTimings = append(monitoredSinkStats.CompressedTimings, getCompressedTiming(protoSinkStats.GetCompressedTiming()))

globals.StatsData.MonitoredSinkStats[sink] = monitoredSinkStats
}

func getApiSpecData(apiSpec *protos.APISpec) (*protos.DataSchema, string, *protos.DataSchema, []*protos.APIAuthType) {
if apiSpec == nil {
return nil, "", nil, nil
Expand Down
8 changes: 7 additions & 1 deletion lib/agent/grpc/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,13 @@ func (s *server) OnUser(ctx context.Context, req *protos.User) (*emptypb.Empty,
}

func (s *server) OnAttackDetected(ctx context.Context, req *protos.AttackDetected) (*emptypb.Empty, error) {
go cloud.SendAttackDetectedEvent(req)
cloud.SendAttackDetectedEvent(req)
storeAttackStats(req)
return &emptypb.Empty{}, nil
}

func (s *server) OnMonitoredSinkStats(ctx context.Context, req *protos.MonitoredSinkStats) (*emptypb.Empty, error) {
storeSinkStats(req)
return &emptypb.Empty{}, nil
}

Expand Down
16 changes: 16 additions & 0 deletions lib/ipc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ service Aikido {
rpc GetCloudConfig(CloudConfigUpdatedAt) returns (CloudConfig);
rpc OnUser(User) returns (google.protobuf.Empty);
rpc OnAttackDetected(AttackDetected) returns (google.protobuf.Empty);
rpc OnMonitoredSinkStats(MonitoredSinkStats) returns (google.protobuf.Empty);
rpc OnMiddlewareInstalled(google.protobuf.Empty) returns (google.protobuf.Empty);
}

Expand Down Expand Up @@ -46,6 +47,21 @@ message RequestMetadataShutdown {
APISpec apiSpec = 6;
}

message CompressedTiming {
double averageInMs = 1;
map<string, double> percentiles = 2;
}

message MonitoredSinkStats {
string sink = 1;
int32 attacksDetected = 2;
int32 attacksBlocked = 3;
int32 interceptorThrewError = 4;
int32 withoutContext = 5;
int32 total = 6;
CompressedTiming compressedTiming = 7;
}

message RateLimiting {
bool enabled = 1;
}
Expand Down
4 changes: 4 additions & 0 deletions lib/php-extension/Action.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ ACTION_STATUS Action::Execute(std::string &event) {
return CONTINUE;
}

bool Action::IsDetection(std::string &event) {
return !event.empty();
}

void Action::Reset() {
exit = false;
block = false;
Expand Down
4 changes: 4 additions & 0 deletions lib/php-extension/Aikido.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ PHP_MSHUTDOWN_FUNCTION(aikido) {
}

PHP_RINIT_FUNCTION(aikido) {
ScopedTimer scopedTimer("request_init");

AIKIDO_LOG_DEBUG("RINIT started!\n");

if (AIKIDO_GLOBAL(disable) == true) {
Expand All @@ -66,6 +68,8 @@ PHP_RINIT_FUNCTION(aikido) {
}

PHP_RSHUTDOWN_FUNCTION(aikido) {
ScopedTimer scopedTimer("request_shutdown");

AIKIDO_LOG_DEBUG("RSHUTDOWN started!\n");

if (AIKIDO_GLOBAL(disable) == true) {
Expand Down
19 changes: 19 additions & 0 deletions lib/php-extension/Environment.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#include "Includes.h"

// Minimum value to use for reporting once every X requests the collected stats to Agent
// As the report_stats_interval_to_agent is configurable, this define is used to ensure that the configured interval is NEVER less that 50 requests
#define MIN_REPORT_STATS_INTERVAL_TO_AGENT 50

std::string GetLaravelEnvVariable(const std::string& env_key) {
zval env_value;
if (!CallPhpFunctionWithOneParam("getenv", env_key, &env_value) || Z_TYPE(env_value) != IS_STRING) {
Expand Down Expand Up @@ -44,6 +48,20 @@ bool GetEnvBool(const std::string& env_key, bool default_value) {
return default_value;
}

unsigned int GetEnvNumber(const std::string& env_key, unsigned int default_value) {
std::string env_value = GetEnvVariable(env_key.c_str());
if (!env_value.empty()) {
try {
unsigned int number = std::stoi(env_value);
if (number <= MIN_REPORT_STATS_INTERVAL_TO_AGENT) {
return MIN_REPORT_STATS_INTERVAL_TO_AGENT;
}
}
catch (...) {}
}
return default_value;
}

void LoadEnvironment() {
if (GetEnvBool("AIKIDO_DEBUG", false)) {
AIKIDO_GLOBAL(log_level_str) = "DEBUG";
Expand All @@ -62,4 +80,5 @@ void LoadEnvironment() {
AIKIDO_GLOBAL(token) = GetEnvString("AIKIDO_TOKEN", "");
AIKIDO_GLOBAL(endpoint) = GetEnvString("AIKIDO_ENDPOINT", "https://guard.aikido.dev/");
AIKIDO_GLOBAL(config_endpoint) = GetEnvString("AIKIDO_REALTIME_ENDPOINT", "https://runtime.aikido.dev/");
AIKIDO_GLOBAL(report_stats_interval_to_agent) = GetEnvNumber("AIKIDO_REPORT_STATS_INTERVAL", 10000);
}
5 changes: 4 additions & 1 deletion lib/php-extension/GoWrappers.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#include "Includes.h"

GoString GoCreateString(std::string& s) {
GoString GoCreateString(const std::string& s) {
return GoString{s.c_str(), s.length()};
}

GoSlice GoCreateSlice(const std::vector<int64_t>& v) {
return GoSlice{ (void*)v.data(), v.size(), v.capacity() };
}
/*
Callback wrapper called by the RequestProcessor (GO) whenever it needs data from PHP (C++ extension).
*/
Expand Down
30 changes: 23 additions & 7 deletions lib/php-extension/Handle.cpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
#include "Includes.h"
#include "include/Stats.h"

ZEND_NAMED_FUNCTION(aikido_generic_handler) {
ScopedTimer scopedTimer;

AIKIDO_LOG_DEBUG("Aikido generic handler started!\n");

zif_handler original_handler = nullptr;
aikido_handler post_handler = nullptr;

std::string sink;
std::string outputEvent;
bool caughtException = false;

Expand Down Expand Up @@ -53,6 +57,9 @@ ZEND_NAMED_FUNCTION(aikido_generic_handler) {
return;
}

sink = scope_name;
scopedTimer.SetSink(sink);

AIKIDO_LOG_DEBUG("Calling handler for \"%s\"!\n", scope_name.c_str());

EVENT_ID eventId = NO_EVENT_ID;
Expand All @@ -68,10 +75,14 @@ ZEND_NAMED_FUNCTION(aikido_generic_handler) {
if (eventId != NO_EVENT_ID) {
std::string outputEvent;
requestProcessor.SendEvent(eventId, outputEvent);
if (requestProcessor.IsBlockingEnabled() && action.Execute(outputEvent) == BLOCK) {
// exit generic handler and do not call the original handler
// thus blocking the execution
return;
if (action.IsDetection(outputEvent)) {
stats[sink].IncrementAttacksDetected();
if (requestProcessor.IsBlockingEnabled() && action.Execute(outputEvent) == BLOCK) {
stats[sink].IncrementAttacksBlocked();
// exit generic handler and do not call the original handler
// thus blocking the execution
return;
}
}
}
} catch (const std::exception& e) {
Expand All @@ -80,7 +91,9 @@ ZEND_NAMED_FUNCTION(aikido_generic_handler) {
}

if (original_handler) {
scopedTimer.Stop();
original_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU);
scopedTimer.Start();

if (!caughtException && post_handler) {
EVENT_ID eventId = NO_EVENT_ID;
Expand All @@ -95,9 +108,12 @@ ZEND_NAMED_FUNCTION(aikido_generic_handler) {
if (eventId != NO_EVENT_ID) {
std::string output;
requestProcessor.SendEvent(eventId, output);
if (requestProcessor.IsBlockingEnabled()) {
action.Execute(output);
}
if (action.IsDetection(output)) {
stats[sink].IncrementAttacksDetected();
if (requestProcessor.IsBlockingEnabled() && action.Execute(output) == BLOCK) {
stats[sink].IncrementAttacksBlocked();
}
}
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions lib/php-extension/HandleShouldBlockRequest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ bool GetBlockingStatus() {
}

ZEND_FUNCTION(should_block_request) {
ScopedTimer scopedTimer("should_block_request");

if (AIKIDO_GLOBAL(disable) == true) {
return;
}
Expand Down
2 changes: 2 additions & 0 deletions lib/php-extension/HandleUsers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ bool SendUserEvent(std::string id, std::string username) {
// Receives two parameters: "id" (string) and "name" (string, optional).
// Returns true if the setting of the user succeeded, false otherwise.
ZEND_FUNCTION(set_user) {
ScopedTimer scopedTimer("set_user");

if (AIKIDO_GLOBAL(disable) == true) {
RETURN_BOOL(false);
}
Expand Down
25 changes: 23 additions & 2 deletions lib/php-extension/RequestProcessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ bool RequestProcessor::IsBlockingEnabled() {
return ret;
}

bool RequestProcessor::ReportStats() {
AIKIDO_LOG_INFO("Reporting stats to Aikido Request Processor...\n");

for (const auto& [sink, sinkStats] : stats) {
AIKIDO_LOG_INFO("Reporting stats for sink \"%s\" to Aikido Request Processor...\n", sink.c_str());
requestProcessorReportStatsFn(GoCreateString(sink), sinkStats.attacksDetected, sinkStats.attacksBlocked, sinkStats.interceptorThrewError, sinkStats.withoutContext, sinkStats.timings.size(), GoCreateSlice(sinkStats.timings));
}
stats.clear();
return true;
}

bool RequestProcessor::Init() {
if (this->initFailed) {
return false;
Expand All @@ -101,12 +112,14 @@ bool RequestProcessor::Init() {
this->requestProcessorConfigUpdateFn = (RequestProcessorConfigUpdateFn)dlsym(libHandle, "RequestProcessorConfigUpdate");
this->requestProcessorOnEventFn = (RequestProcessorOnEventFn)dlsym(libHandle, "RequestProcessorOnEvent");
this->requestProcessorGetBlockingModeFn = (RequestProcessorGetBlockingModeFn)dlsym(libHandle, "RequestProcessorGetBlockingMode");
this->requestProcessorReportStatsFn = (RequestProcessorReportStats)dlsym(libHandle, "RequestProcessorReportStats");
this->requestProcessorUninitFn = (RequestProcessorUninitFn)dlsym(libHandle, "RequestProcessorUninit");
if (!requestProcessorInitFn ||
!this->requestProcessorContextInitFn ||
!this->requestProcessorConfigUpdateFn ||
!this->requestProcessorOnEventFn ||
!this->requestProcessorGetBlockingModeFn ||
!this->requestProcessorReportStatsFn ||
!this->requestProcessorUninitFn) {
AIKIDO_LOG_ERROR("Error loading symbols from the Aikido Request Processor library!\n");
this->initFailed = true;
Expand Down Expand Up @@ -135,10 +148,15 @@ bool RequestProcessor::RequestInit() {
return false;
}

requestInitialized = true;
this->requestInitialized = true;
this->numberOfRequests++;

ContextInit();
SendPreRequestEvent();

if ((this->numberOfRequests % AIKIDO_GLOBAL(report_stats_interval_to_agent)) == 0) {
requestProcessor.ReportStats();
}
return true;
}

Expand All @@ -161,14 +179,17 @@ void RequestProcessor::RequestShutdown() {

LoadConfigOnce();
SendPostRequestEvent();
requestInitialized = false;
this->requestInitialized = false;
}

void RequestProcessor::Uninit() {
if (!this->libHandle) {
return;
}
if (!this->initFailed && this->requestProcessorUninitFn) {
AIKIDO_LOG_INFO("Reporting final stats to Aikido Request Processor...\n");
this->ReportStats();

AIKIDO_LOG_INFO("Calling uninit for Aikido Request Processor...\n");
this->requestProcessorUninitFn();
}
Expand Down
Loading

0 comments on commit 6c4f485

Please sign in to comment.