Skip to content

Commit bc94f98

Browse files
Merge pull request #224 from AikidoSec/report-total-rate-limited
Count total rate limited requests
2 parents 0f19607 + a9abf9c commit bc94f98

5 files changed

Lines changed: 62 additions & 3 deletions

File tree

agent_api/src/main/java/dev/aikido/agent_api/ShouldBlockRequest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import dev.aikido.agent_api.ratelimiting.ShouldRateLimit;
77
import dev.aikido.agent_api.storage.ServiceConfigStore;
88
import dev.aikido.agent_api.storage.ServiceConfiguration;
9+
import dev.aikido.agent_api.storage.statistics.StatisticsStore;
910

1011
public final class ShouldBlockRequest {
1112
private ShouldBlockRequest() {
@@ -54,6 +55,7 @@ public static ShouldBlockRequestResult shouldBlockRequest() {
5455
context.getRouteMetadata(), context.getUser(), context.getRateLimitGroup(), context.getRemoteAddress()
5556
);
5657
if (rateLimitDecision.block()) {
58+
StatisticsStore.incrementRateLimitedStats();
5759
BlockedRequestResult blockedRequestResult = new BlockedRequestResult(
5860
/* type */ "ratelimited",
5961
/* trigger */ rateLimitDecision.trigger(),

agent_api/src/main/java/dev/aikido/agent_api/storage/statistics/Statistics.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public class Statistics {
1414
private int attacksBlocked;
1515
private int attackWavesDetected;
1616
private int attackWavesBlocked;
17+
private int rateLimited;
1718
private long startedAt;
1819

1920
public Statistics(int totalHits, int attacksDetected, int attacksBlocked) {
@@ -22,6 +23,7 @@ public Statistics(int totalHits, int attacksDetected, int attacksBlocked) {
2223
this.attacksBlocked = attacksBlocked;
2324
this.attackWavesDetected = 0;
2425
this.attackWavesBlocked = 0;
26+
this.rateLimited = 0;
2527
this.startedAt = UnixTimeMS.getUnixTimeMS();
2628
}
2729

@@ -80,6 +82,13 @@ public int getAttackWavesBlocked() {
8082
return attackWavesBlocked;
8183
}
8284

85+
public void incrementRateLimitStats() {
86+
this.rateLimited += 1;
87+
}
88+
public int getRateLimitedStats() {
89+
return this.rateLimited;
90+
}
91+
8392
// operations
8493
public void registerCall(String operation, OperationKind kind) {
8594
if (!this.operations.containsKey(operation)) {
@@ -128,7 +137,7 @@ public StatsRecord getRecord() {
128137
return new StatsRecord(
129138
this.startedAt,
130139
endedAt,
131-
new StatsRequestsRecord(totalHits, /* aborted: unknown */ 0, attackStats, attackWaveStats),
140+
new StatsRequestsRecord(totalHits, /* aborted: unknown */ 0, rateLimited, attackStats, attackWaveStats),
132141
getOperations(),
133142
Map.of("breakdown", getIpAddresses()),
134143
Map.of("breakdown", getUserAgents())
@@ -141,6 +150,7 @@ public void clear() {
141150
this.attacksDetected = 0;
142151
this.attackWavesBlocked = 0;
143152
this.attackWavesDetected = 0;
153+
this.rateLimited = 0;
144154
this.startedAt = UnixTimeMS.getUnixTimeMS();
145155
this.operations.clear();
146156
this.ipAddressMatches.clear();
@@ -154,6 +164,7 @@ public record StatsTotalAndBlocked(int total, int blocked) {
154164
public record StatsRequestsRecord(
155165
long total,
156166
long aborted,
167+
long rateLimited,
157168
StatsTotalAndBlocked attacksDetected,
158169
StatsTotalAndBlocked attackWaves
159170
) {

agent_api/src/main/java/dev/aikido/agent_api/storage/statistics/StatisticsStore.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,13 @@ public static void incrementAttackWavesDetected() {
9797
mutex.unlock();
9898
}
9999
}
100+
101+
public static void incrementRateLimitedStats() {
102+
mutex.lock();
103+
try {
104+
stats.incrementRateLimitStats();
105+
} finally {
106+
mutex.unlock();
107+
}
108+
}
100109
}

agent_api/src/test/java/ShouldBlockRequestTest.java

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import dev.aikido.agent_api.context.Context;
55
import dev.aikido.agent_api.context.ContextObject;
66
import dev.aikido.agent_api.context.User;
7+
import dev.aikido.agent_api.storage.RateLimiterStore;
78
import dev.aikido.agent_api.storage.ServiceConfigStore;
9+
import dev.aikido.agent_api.storage.statistics.StatisticsStore;
810
import org.junit.jupiter.api.AfterEach;
911
import org.junit.jupiter.api.BeforeAll;
1012
import org.junit.jupiter.api.Test;
@@ -38,12 +40,16 @@ public SampleContextObject() {
3840
public static void clean() {
3941
Context.set(null);
4042
ServiceConfigStore.updateFromAPIResponse(emptyAPIResponse);
43+
StatisticsStore.clear();
44+
RateLimiterStore.clear();
4145
};
4246

4347
@AfterEach
4448
public void tearDown() throws SQLException {
4549
Context.set(null);
4650
ServiceConfigStore.updateFromAPIResponse(emptyAPIResponse);
51+
StatisticsStore.clear();
52+
RateLimiterStore.clear();
4753
}
4854

4955
@Test
@@ -135,7 +141,7 @@ public void testEndpointsExistButNoMatch() throws SQLException {
135141
public void testEndpointsExistWithMatch() throws SQLException {
136142
Context.set(null);
137143
setEmptyConfigWithEndpointList(List.of(
138-
new Endpoint("GET", "/api/*", 1, 1000, Collections.emptyList(), false, false, false)
144+
new Endpoint("GET", "/api/*", 1, 1000, Collections.emptyList(), false, false, false)
139145
));
140146

141147
// Test with match & rate-limiting disabled :
@@ -144,14 +150,40 @@ public void testEndpointsExistWithMatch() throws SQLException {
144150

145151
Context.set(null);
146152
setEmptyConfigWithEndpointList(List.of(
147-
new Endpoint("GET", "/api/*", 1, 1000, Collections.emptyList(), false, false, true)
153+
new Endpoint("GET", "/api/*", 1, 1000, Collections.emptyList(), false, false, true)
148154
));
149155

150156
// Test with match & rate-limiting enabled :
151157
var res2 = ShouldBlockRequest.shouldBlockRequest();
152158
assertFalse(res2.block());
153159
}
154160

161+
@Test
162+
public void testEndpointsExistAndGetsRateLimited() throws SQLException {
163+
setEmptyConfigWithEndpointList(List.of(
164+
new Endpoint("GET", "/api/*", 2, 1000, Collections.emptyList(), false, false, true)
165+
));
166+
167+
// Test with match
168+
Context.set(new SampleContextObject());
169+
var res1 = ShouldBlockRequest.shouldBlockRequest();
170+
assertFalse(res1.block());
171+
172+
// Test with match
173+
var res2 = ShouldBlockRequest.shouldBlockRequest();
174+
assertFalse(res2.block());
175+
assertEquals(0, StatisticsStore.getStatsRecord().requests().rateLimited());
176+
177+
var res3 = ShouldBlockRequest.shouldBlockRequest();
178+
var res4 = ShouldBlockRequest.shouldBlockRequest();
179+
assertTrue(res3.block());
180+
assertTrue(res4.block());
181+
assertEquals("ip", res3.data().trigger());
182+
assertEquals("192.168.1.1", res3.data().ip());
183+
assertEquals("ratelimited", res3.data().type());
184+
assertEquals(2, StatisticsStore.getStatsRecord().requests().rateLimited());
185+
}
186+
155187
@Test
156188
public void testThreadClientInvalid() throws SQLException {
157189
Context.set(new SampleContextObject());

agent_api/src/test/java/storage/StatisticsTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,14 @@ public void testClear() {
3434
stats.incrementAttacksDetected("test2");
3535
stats.incrementAttacksDetected("test1");
3636
stats.incrementAttacksDetected("test1");
37+
stats.incrementRateLimitStats();
38+
stats.incrementRateLimitStats();
3739
assertEquals(3, stats.getAttacksDetected());
3840
assertEquals(2, stats.getAttacksBlocked());
3941
assertEquals(20, stats.getTotalHits());
4042
assertEquals(2, stats.getOperations().get("test1").getAttacksDetected().get("total"));
4143
assertEquals(1, stats.getOperations().get("test1").getAttacksDetected().get("blocked"));
44+
assertEquals(2, stats.getRateLimitedStats());
4245

4346
assertFalse(stats.getOperations().containsKey("test2"));
4447
// Reset :
@@ -47,6 +50,7 @@ public void testClear() {
4750
assertEquals(0, stats.getAttacksBlocked());
4851
assertEquals(0, stats.getAttacksDetected());
4952
assertEquals(0, stats.getTotalHits());
53+
assertEquals(0, stats.getRateLimitedStats());
5054

5155
}
5256

@@ -56,6 +60,7 @@ public void testConstructor() {
5660
assertEquals(100, stats2.getTotalHits());
5761
assertEquals(5, stats2.getAttacksDetected());
5862
assertEquals(1, stats2.getAttacksBlocked());
63+
assertEquals(0, stats.getRateLimitedStats());
5964
}
6065

6166
@Test

0 commit comments

Comments
 (0)